Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pace26io"
version = "0.1.0"
version = "0.2.0"
edition = "2024"
authors = ["Manuel Penschuck"]
description = "Utilities to read PACE26 instances and write answers"
Expand All @@ -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]
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -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:

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions examples/tiny01.nw
Original file line number Diff line number Diff line change
Expand Up @@ -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]]]
63 changes: 63 additions & 0 deletions src/pace/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 },

Expand Down Expand Up @@ -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::<f64>().ok())?;
if param_a < 0.0 {
return None;
}
let param_b = parts.next().and_then(|x| x.parse::<usize>().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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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";
Expand Down
31 changes: 22 additions & 9 deletions src/pace/simplified.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ pub struct Instance<B: TreeBuilder> {
pub num_leaves: usize,
pub trees: Vec<B::Node>,
pub tree_decomposition: Option<TreeDecomposition>,

/// Represents parameters (a, b) where an approximate solution of size at most `a * opt + b` is allowable
pub approx: Option<(f64, usize)>,
}

impl<B: TreeBuilder> Instance<B> {
Expand All @@ -32,6 +35,7 @@ impl<B: TreeBuilder> Instance<B> {
num_leaves: 0,
trees: Vec::with_capacity(2),
tree_decomposition: None,
approx: None,
};

let mut visitor = Visitor {
Expand Down Expand Up @@ -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;
Expand All @@ -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 => {
Expand All @@ -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;
Expand Down Expand Up @@ -133,14 +142,17 @@ 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")]
NoLeaves,

#[error("No header before first tree")]
NoHeader,

#[error("Multiple approx lines (#a) found")]
MultipleApprox,
}

#[cfg(test)]
Expand All @@ -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)));
}
}