From 70e711e42e05d070a05328ebd7cd7dd34802812f Mon Sep 17 00:00:00 2001 From: Manuel Penschuck Date: Tue, 6 Jan 2026 13:58:02 +0100 Subject: [PATCH 1/3] Improve documentation --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index db9fc65..67101f5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # PACE 2026 I/O Crate This crate implements parsers and writers for the [PACE 2026 file format](https://pacechallenge.org/2026/format/). -It was originally developed for the official PACE tools (e.g., verifier and stride). +It was originally developed for the official PACE tools (e.g., verifier and [Stride](https://github.com/manpen/pace26stride)). As such, it offers a great deal of flexibility including quite pedantic parsing modes. Most users should stay away from this mess and rather use the simplified reader interface: @@ -28,18 +28,18 @@ let instance = Instance::try_read(&mut input, &mut tree_builder) println!("# Found {} trees", instance.trees.len()); ``` -This interface will ignore most parser warnings and only raise errors if parsing cannot continue. We recommend the `stride` tool to debug broken instances. +This interface will ignore most parser warnings and only raise errors if parsing cannot continue. +We recommend the [Stride tool](https://github.com/manpen/pace26stride) to debug broken instances. ## Tree representation -We offer only rudamentary tree representations, more specifically -[`binary_tree::BinTree`] and [`binary_tree::IndexedBinTree`]. The latter also stores -node ids of internal nodes, used --for instance-- for graph parameters. +We offer only rudimentary tree representations, more specifically [`binary_tree::BinTree`] and [`binary_tree::IndexedBinTree`]. +The latter also stores node ids of internal nodes, used --for instance-- for graph parameters. We expect that solvers typically need more control over their data structures. -For this reason, the crate is designed to make implementating own tree structures straightforward. +For this reason, the crate is designed to make implementation of own tree structures straightforward. You need to provide - - A node type which respresents both inner nodes and leafes. It needs to implement [`binary_tree::TopDownCursor`] and --if applicable-- [`binary_tree::TreeWithNodeIdx`]. + - A node type which represents both inner nodes and leaves. It needs to implement [`binary_tree::TopDownCursor`] and --if applicable-- [`binary_tree::TreeWithNodeIdx`]. - A struct implementing [`binary_tree::TreeBuilder`]. ## Writing Newick strings From 63c63e005b1f4aedf41ec59389f358461d81b2e7 Mon Sep 17 00:00:00 2001 From: Manuel Penschuck Date: Tue, 6 Jan 2026 13:59:16 +0100 Subject: [PATCH 2/3] Increase version --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ecdcc2b..9a2c74e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pace26io" -version = "0.1.0" +version = "0.1.1" edition = "2024" authors = ["Manuel Penschuck"] description = "Utilities to read PACE26 instances and write answers" @@ -11,7 +11,7 @@ exclude = ["/.github"] [dependencies] serde = "1.0.228" -serde_json = "1.0.145" +serde_json = "1.0.148" thiserror = "2.0.17" [dev-dependencies] From 2f91141091bd2afc5b725c64dcd0d53a23bd332f Mon Sep 17 00:00:00 2001 From: Manuel Penschuck Date: Mon, 19 Jan 2026 21:44:52 +0100 Subject: [PATCH 3/3] Add support for approx lines (#a) --- Cargo.toml | 2 +- examples/tiny01.nw | 1 + src/pace/reader.rs | 63 ++++++++++++++++++++++++++++++++++++++++++ src/pace/simplified.rs | 31 +++++++++++++++------ 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9a2c74e..1a5406e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pace26io" -version = "0.1.1" +version = "0.2.0" edition = "2024" authors = ["Manuel Penschuck"] description = "Utilities to read PACE26 instances and write answers" diff --git a/examples/tiny01.nw b/examples/tiny01.nw index f2fa395..217c457 100644 --- a/examples/tiny01.nw +++ b/examples/tiny01.nw @@ -2,6 +2,7 @@ #s name "tiny01" #s desc "Example shown on https://pacechallenge.org" #p 2 6 +#a 1.2000 1337 (((5,6),(3,4)),(1,2)); (((((4,2),1),5),3),6); #x treedecomp [2,[[8,16],[8,11,16],[1,11,15],[2,11,16],[7,8,11],[8,10,16],[3,10,13],[4,10,16],[8,9],[5,9,14],[6,9,12]],[[1,2],[1,6],[1,9],[2,3],[2,4],[2,5],[6,7],[6,8],[9,10],[9,11]]] diff --git a/src/pace/reader.rs b/src/pace/reader.rs index 9d9f6f6..f220335 100644 --- a/src/pace/reader.rs +++ b/src/pace/reader.rs @@ -49,6 +49,10 @@ pub trait InstanceVisitor { fn visit_header(&mut self, _lineno: usize, _num_trees: usize, _num_leaves: usize) -> Action { Action::Continue } + fn visit_approx_line(&mut self, _lineno: usize, _param_a: f64, _param_b: usize) -> Action { + Action::Continue + } + fn visit_tree(&mut self, _lineno: usize, _line: &str) -> Action { Action::Continue } @@ -89,6 +93,9 @@ pub enum ReaderError { #[error("Identified line {} as parameter line. Expected '#x {{key}}: {{value}}'", lineno+1)] InvalidParameterLine { lineno: usize }, + #[error("Identified line {} as approx line. Expected '#a {{a}} {{b}}'", lineno+1)] + InvalidApproxLine { lineno: usize }, + #[error("Unknown parameter in line {}: {key}'", lineno+1)] UnknownParameter { lineno: usize, key: String }, @@ -117,6 +124,21 @@ fn try_parse_header(line: &str) -> Option<(usize, usize)> { Some((num_trees, num_leaves)) } +fn try_parse_approx(line: &str) -> Option<(f64, usize)> { + let mut parts = line.split(' '); + if parts.next()? != "#a" { + return None; + } + + let param_a = parts.next().and_then(|x| x.parse::().ok())?; + if param_a < 0.0 { + return None; + } + let param_b = parts.next().and_then(|x| x.parse::().ok())?; + + Some((param_a, param_b)) +} + /// Expects a line `#X {key} {value}` and returns ({key}, {value}) if found fn try_split_key_value(line: &str) -> Option<(&str, &str)> { let split = line[3..].find(' ')? + 3; @@ -193,6 +215,13 @@ impl<'a, V: InstanceVisitor> InstanceReader<'a, V> { } else { return Err(ReaderError::InvalidStrideLine { lineno }); } + } else if content.starts_with("#a") { + // stride line in the format "#s key: value" + if let Some((a, b)) = try_parse_approx(content) { + visit!(visit_approx_line, lineno, a, b); + } else { + return Err(ReaderError::InvalidApproxLine { lineno }); + } } else if content.starts_with("#x") { if let Some((key, value)) = try_split_key_value(content) { match key { @@ -251,6 +280,7 @@ mod tests { pub unrecognized_lines: Vec<(usize, String)>, pub stride_lines: Vec<(usize, String, String, String)>, pub param_tree_decomp: Option<(usize, TreeDecomposition)>, + pub approx_lines: Vec<(usize, f64, usize)>, } impl InstanceVisitor for TestVisitor { @@ -280,6 +310,11 @@ mod tests { Action::Continue } + fn visit_approx_line(&mut self, lineno: usize, param_a: f64, param_b: usize) -> Action { + self.approx_lines.push((lineno, param_a, param_b)); + Action::Continue + } + fn visit_stride_line( &mut self, lineno: usize, @@ -370,6 +405,34 @@ mod tests { ); } + #[test] + fn input_with_approx_line() { + let input = "#p 2 3\n#s stride_key somevalue\n#a 1.2345 42\n(1);\n"; + let mut visitor = TestVisitor::default(); + let mut reader = InstanceReader::new(&mut visitor); + reader.read(input.as_bytes()).unwrap(); + + assert_eq!(visitor.approx_lines, vec![(2, 1.2345, 42)]); + } + + #[test] + fn input_with_invalid_approx_line() { + for input in [ + "#p 2 3\n#s stride_key somevalue\n#a -1.2345 42\n(1);\n", + "#a foo 3", + "#a 1.234 foo", + "#a 1.2345 -4", + ] { + let mut visitor = TestVisitor::default(); + let mut reader = InstanceReader::new(&mut visitor); + let res = reader.read(input.as_bytes()); + assert!( + matches!(res.unwrap_err(), ReaderError::InvalidApproxLine { .. }), + "{input:?}" + ); + } + } + #[test] fn input_with_stride_line() { let input = "#p 2 3\n#s stride_key somevalue\n(1);\n"; diff --git a/src/pace/simplified.rs b/src/pace/simplified.rs index 0f6f12a..1bb2da1 100644 --- a/src/pace/simplified.rs +++ b/src/pace/simplified.rs @@ -21,6 +21,9 @@ pub struct Instance { pub num_leaves: usize, pub trees: Vec, pub tree_decomposition: Option, + + /// Represents parameters (a, b) where an approximate solution of size at most `a * opt + b` is allowable + pub approx: Option<(f64, usize)>, } impl Instance { @@ -32,6 +35,7 @@ impl Instance { num_leaves: 0, trees: Vec::with_capacity(2), tree_decomposition: None, + approx: None, }; let mut visitor = Visitor { @@ -60,12 +64,7 @@ struct Visitor<'a, B: TreeBuilder> { } impl<'a, B: TreeBuilder> InstanceVisitor for Visitor<'a, B> { - fn visit_header( - &mut self, - _lineno: usize, - _num_trees: usize, - num_leaves: usize, - ) -> super::reader::Action { + fn visit_header(&mut self, _lineno: usize, _num_trees: usize, num_leaves: usize) -> Action { if self.num_leaves.is_some() { self.error = Some(SimplifiedReaderError::MultipleHeaders); return Action::Terminate; @@ -81,7 +80,7 @@ impl<'a, B: TreeBuilder> InstanceVisitor for Visitor<'a, B> { Action::Continue } - fn visit_tree(&mut self, _lineno: usize, line: &str) -> super::reader::Action { + fn visit_tree(&mut self, _lineno: usize, line: &str) -> Action { let num_leaves = match self.num_leaves { Some(x) => x, None => { @@ -105,7 +104,17 @@ impl<'a, B: TreeBuilder> InstanceVisitor for Visitor<'a, B> { self.instance.trees.push(tree); - super::reader::Action::Continue + Action::Continue + } + + fn visit_approx_line(&mut self, _lineno: usize, param_a: f64, param_b: usize) -> Action { + if self.instance.approx.is_some() { + self.error = Some(SimplifiedReaderError::MultipleApprox); + return Action::Terminate; + } + + self.instance.approx = Some((param_a, param_b)); + Action::Continue } const VISIT_PARAM_TREE_DECOMPOSITION: bool = true; @@ -133,7 +142,7 @@ pub enum SimplifiedReaderError { #[error(transparent)] IO(#[from] std::io::Error), - #[error("Multiple headers found")] + #[error("Multiple headers (#p) found")] MultipleHeaders, #[error("Header indicates no leaves")] @@ -141,6 +150,9 @@ pub enum SimplifiedReaderError { #[error("No header before first tree")] NoHeader, + + #[error("Multiple approx lines (#a) found")] + MultipleApprox, } #[cfg(test)] @@ -162,5 +174,6 @@ mod test { assert_eq!(instance.num_leaves, 6); assert_eq!(instance.trees.len(), 2); assert_eq!(instance.tree_decomposition.unwrap().treewidth, 2); + assert_eq!(instance.approx, Some((1.2, 1337))); } }