diff --git a/README.md b/README.md index da5f664..42cb4fe 100644 --- a/README.md +++ b/README.md @@ -18,17 +18,17 @@ A Rust LP file parser leveraging [PEST](https://docs.rs/pest/latest/pest/) and a - Constraints - Bounds - Variable Types: Integer, Generals, Lower Bounded, Upper Bounded, Free & Upper and Lower Bounded +- Semi-continuous ## TODOs List - Remaining LP format changes: - - Semi-continuous - - - - - Special ordered sets: SOS - Keyword: `SOS` - Example: `Sos101: 1.2 x1 + 1.3 x2 + 1.4 x4 = S1` - Lazy Constraints + - Semi-Continuous + - See details in FICO® Xpress - Extensions: - Compares two LP files - CLI diff --git a/resources/semi_continuous.lp b/resources/semi_continuous.lp new file mode 100644 index 0000000..4b2e47c --- /dev/null +++ b/resources/semi_continuous.lp @@ -0,0 +1,18 @@ +Minimize + b_5829890_x2 + + b_5880854_x2 +Subject To + - 2 b_5829890_x2 + 2 b_5829890_x1 <= -64 +b_5880854_x2 - b_5880854_x1 >= 32 +Bounds +b_5829890_x1 >= 10 +10 >= b_5880854_x1 +1014 <= b_5829890_x2 <= 1917 +1014 <= b_5880854_x2 <= 1917 +Generals +b_5829890_x2 b_5880854_x2 +Binary +b_5829890_x1 +Semi-continuous +x1 x2 x3 +End diff --git a/src/lp_file_format.pest b/src/lp_file_format.pest index ec5cd59..20db3ca 100644 --- a/src/lp_file_format.pest +++ b/src/lp_file_format.pest @@ -43,14 +43,15 @@ PROBLEM_SENSE = _{ NEWLINE* ~ (MIN_SENSE | MAX_SENSE) } // https://www.ibm.com/docs/en/icos/22.1.1?topic=representation-variable-names-in-lp-file-format // alphanumeric (a-z, A-Z, 0-9) or one of these symbols: !"#$%&/,.;?@_`'{}()|~' VALID_CHARS = _{ - !(FREE | END | CONSTRAINT_PREFIX) ~ (ASCII_ALPHANUMERIC | "!" | "#" | "$" | "%" | "&" | "(" | ")" | "," | "." | ";" | "?" | "@" | "_" | "‘" | "’" | "{" | "}" | "~") + !WHITESPACE ~ (ASCII_ALPHANUMERIC | "!" | "#" | "$" | "%" | "&" | "(" | ")" | "," | "." | ";" | "?" | "@" | "_" | "‘" | "’" | "{" | "}" | "~") } CONSTRAINT_PREFIX = _{ (^"subject to" | ^"such that" | ^"S.T.") ~ COLON? ~ NEWLINE? } VARIABLE = { - !(FREE | END | CONSTRAINT_PREFIX | GENERALS_PREFIX | BINARIES_PREFIX) ~ VALID_CHARS{1, 255} + !(FREE | END | CONSTRAINT_PREFIX | GENERALS_PREFIX | BINARIES_PREFIX | SEMI_CONTINUOUS_PREFIX) ~ VALID_CHARS{1, 255} } +SEMI_VARIABLE = { !(FREE | END | CONSTRAINT_PREFIX | GENERALS_PREFIX | BINARIES_PREFIX | SEMI_CONTINUOUS_PREFIX) ~ VALID_CHARS ~ (!WHITESPACE ~ VALID_CHARS) } // Objective function // https://www.ibm.com/docs/en/icos/22.1.1?topic=representation-objective-in-lp-file-format @@ -109,12 +110,18 @@ GENERALS = { NEWLINE ~ GENERALS_PREFIX ~ (NEWLINE ~ VARIABLE)+ } BINARIES_PREFIX = _{ ^"Binar" ~ (^"ies" | ^"y") } BINARIES = { NEWLINE ~ BINARIES_PREFIX ~ (NEWLINE ~ VARIABLE)+ } +// Semi-Continuous +// To specify any of the variables as semi-continuous variables, that is as variables that +// may take the value 0 or values between the specified lower and upper bounds +SEMI_CONTINUOUS_PREFIX = _{ ^"SEMI" ~ (^"S" | ^"-CONTINUOUS") } +SEMI_CONTINUOUS = { NEWLINE ~ SEMI_CONTINUOUS_PREFIX ~ (NEWLINE? ~ SEMI_VARIABLE)+ } + // End of file // https://www.ibm.com/docs/en/icos/22.1.1?topic=representation-end-file-in-lp-file-format EOF = _{ NEWLINE* ~ END } // Global -OPTIONAL_SECTIONS = _{ BOUNDS? ~ INTEGERS? ~ GENERALS? ~ BINARIES? ~ EOF? } +OPTIONAL_SECTIONS = _{ BOUNDS? ~ INTEGERS? ~ GENERALS? ~ BINARIES? ~ SEMI_CONTINUOUS? ~ EOF? } LP_FILE = { LP_PROBLEM_NAME? ~ COMMENTS* ~ PROBLEM_SENSE ~ OBJECTIVES ~ CONSTRAINTS ~ OPTIONAL_SECTIONS } diff --git a/src/lp_parts.rs b/src/lp_parts.rs index b112b3d..f85b098 100644 --- a/src/lp_parts.rs +++ b/src/lp_parts.rs @@ -122,6 +122,14 @@ pub fn compose(pair: Pair<'_, Rule>, mut parsed: LPDefinition) -> anyhow::Result } } } + // Problem Semi-continuous + Rule::SEMI_CONTINUOUS => { + for bin_pair in pair.into_inner() { + if matches!(bin_pair.as_rule(), Rule::SEMI_VARIABLE) { + parsed.set_var_bounds(bin_pair.as_str(), VariableType::SemiContinuous); + } + } + } // Otherwise, skip! _ => (), } diff --git a/src/model.rs b/src/model.rs index d4eece8..f8bca9d 100644 --- a/src/model.rs +++ b/src/model.rs @@ -26,6 +26,8 @@ pub enum VariableType { #[default] // General variable [0, +Infinity] General, + // Semi-continuous + SemiContinuous, } #[derive(Debug)] @@ -102,14 +104,18 @@ impl LPDefinition { } pub fn add_variable(&mut self, name: &str) { - self.variables.entry(name.to_string()).or_default(); + if !name.is_empty() { + self.variables.entry(name.trim().to_string()).or_default(); + } } pub fn set_var_bounds(&mut self, name: &str, kind: VariableType) { - match self.variables.entry(name.to_string()) { - Entry::Occupied(k) => *k.into_mut() = kind, - Entry::Vacant(k) => { - k.insert(kind); + if !name.is_empty() { + match self.variables.entry(name.trim().to_string()) { + Entry::Occupied(k) => *k.into_mut() = kind, + Entry::Vacant(k) => { + k.insert(kind); + } } } } diff --git a/tests/test_from_file.rs b/tests/test_from_file.rs index 7b16f40..eabc233 100644 --- a/tests/test_from_file.rs +++ b/tests/test_from_file.rs @@ -12,7 +12,7 @@ fn afiro() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 3); assert_eq!(result.constraints.len(), 27); - assert_eq!(result.variables.len(), 44); + assert_eq!(result.variables.len(), 32); } #[test] @@ -22,7 +22,7 @@ fn afiro_ext() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 4); assert_eq!(result.constraints.len(), 27); - assert_eq!(result.variables.len(), 66); + assert_eq!(result.variables.len(), 47); } #[test] @@ -32,7 +32,7 @@ fn boeing1() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 348); - assert_eq!(result.variables.len(), 856); + assert_eq!(result.variables.len(), 473); } #[test] @@ -42,7 +42,7 @@ fn boeing2() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 140); - assert_eq!(result.variables.len(), 280); + assert_eq!(result.variables.len(), 162); } #[test] @@ -52,7 +52,7 @@ fn fit1d() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 24); - assert_eq!(result.variables.len(), 2053); + assert_eq!(result.variables.len(), 1026); } #[test] @@ -62,7 +62,7 @@ fn fit2d() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 25); - assert_eq!(result.variables.len(), 21001); + assert_eq!(result.variables.len(), 10500); } #[test] @@ -72,7 +72,7 @@ fn kb2() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 43); - assert_eq!(result.variables.len(), 79); + assert_eq!(result.variables.len(), 41); } #[test] @@ -82,7 +82,7 @@ fn pulp() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 49); - assert_eq!(result.variables.len(), 86); + assert_eq!(result.variables.len(), 62); } #[test] @@ -92,7 +92,7 @@ fn pulp2() { assert_eq!(result.problem_sense, Sense::Maximize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 7); - assert_eq!(result.variables.len(), 148); + assert_eq!(result.variables.len(), 139); } #[test] @@ -102,7 +102,7 @@ fn sc50a() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 49); - assert_eq!(result.variables.len(), 70); + assert_eq!(result.variables.len(), 48); } #[test] @@ -118,7 +118,7 @@ fn no_end_section() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 4); assert_eq!(result.constraints.len(), 2); - assert_eq!(result.variables.len(), 6); + assert_eq!(result.variables.len(), 3); } #[test] @@ -128,7 +128,7 @@ fn model2() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 4); - assert_eq!(result.variables.len(), 16); + assert_eq!(result.variables.len(), 8); } #[test] @@ -138,7 +138,7 @@ fn limbo() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 2); assert_eq!(result.constraints.len(), 2); - assert_eq!(result.variables.len(), 8); + assert_eq!(result.variables.len(), 5); } #[test] @@ -148,7 +148,7 @@ fn obj3_2cons() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 4); assert_eq!(result.constraints.len(), 2); - assert_eq!(result.variables.len(), 6); + assert_eq!(result.variables.len(), 3); } #[test] @@ -158,7 +158,7 @@ fn obj_2cons_only_binary_vars() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 2); assert_eq!(result.constraints.len(), 2); - assert_eq!(result.variables.len(), 7); + assert_eq!(result.variables.len(), 4); } #[test] @@ -168,7 +168,7 @@ fn obj_2cons_all_variable_types() { assert_eq!(result.problem_sense, Sense::Minimize); assert_eq!(result.objectives.len(), 2); assert_eq!(result.constraints.len(), 2); - assert_eq!(result.variables.len(), 7); + assert_eq!(result.variables.len(), 3); } #[test] @@ -178,7 +178,17 @@ fn obj_1cons_all_variables_with_bounds() { assert_eq!(result.problem_sense, Sense::Maximize); assert_eq!(result.objectives.len(), 1); assert_eq!(result.constraints.len(), 1); - assert_eq!(result.variables.len(), 6); + assert_eq!(result.variables.len(), 3); +} + +#[test] +fn semi_continuous() { + let result = read_file_from_resources("semi_continuous.lp").unwrap(); + assert_eq!("", result.problem_name); + assert_eq!(result.problem_sense, Sense::Minimize); + assert_eq!(result.objectives.len(), 2); + assert_eq!(result.constraints.len(), 2); + assert_eq!(dbg!(result.variables).len(), 8); } fn read_file_from_resources(file_name: &str) -> anyhow::Result {