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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ edition = "2024"
authors = ["Manuel Penschuck"]
description = "Utilities to read PACE26 instances and write answers"
license = "GPL-3.0-or-later"
repository = "https://github.com/manpen/pace26io"
homepage = "https://pacechallenge.org/2026/"
exclude = ["/.github"]

[dependencies]
serde = "1.0.228"
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# PACE 2026 I/O Crate

This crate implements parsers and writers for the PACE 2026 file format. It was originally
developped for the official PACE tools (e.g., verifier and stride). 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:

## Simplified reader interface

We offer a simplified interface in [`pace::simplified::Instance`] intend to be used by solver
implementers. To read an instance, you may use:

```rust
use std::{fs::File, io::BufReader};
use pace26io::{binary_tree::*, newick::NewickWriter, pace::simplified::*};

type Builder = IndexedBinTreeBuilder; // If you do not care about inner node indices, use BinTreeBuilder
type Node = <Builder as TreeBuilder>::Node;

// A solver would typically use `std::io::stdin().lock()` instead of reading a file
let mut input = BufReader::new(File::open("examples/tiny01.nw").unwrap());

// Parse instance
let mut tree_builder = Builder::default();
let instance = Instance::try_read(&mut input, &mut tree_builder)
.expect("Valid PACE26 Instance");

println!("# Found {} trees", instance.trees.len());
```

This interface will ignore most parser warnings and only report raise errors if parsing cannot continue. We recommend the `stride` tool 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, which are for instance used by graph parameters.

We expect that solvers will typically need more control over their data structures.
For this reason, the crate is designed to make implementation of own tree structures straight forward.
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 struct implementing [`binary_tree::TreeBuilder`].

## Writing Newick strings

A Newick String writer is provided for each data structure implementing [`binary_tree::TopDownCursor`].
For further details see [`newick::NewickWriter`].
76 changes: 76 additions & 0 deletions examples/normalize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/// This example reads in an instance, orders the trees, the children of each
/// inner node, such that the left child always contains the smallest leaf label.
///
/// To execute it, run `cat examples/tiny01.nw | cargo --example normalize`
use pace26io::{binary_tree::*, newick::NewickWriter, pace::simplified::*};

type Builder = IndexedBinTreeBuilder; // If you do not care about inner node indices, use BinTreeBuilder
type Node = <Builder as TreeBuilder>::Node;

fn main() {
let mut tree_builder = Builder::default();
let instance = Instance::try_read(&mut std::io::stdin().lock(), &mut tree_builder)
.expect("Valid PACE26 Instance");

println!("# Found {} trees", instance.trees.len());
if let Some(td) = instance.tree_decomposition.as_ref() {
println!(
"# Found tree decomposition with treewidth {}, {} bags, and {} edges",
td.treewidth,
td.bags.len(),
td.edges.len()
);
}

for (tree_id, tree) in instance.trees.iter().enumerate() {
let root_id = (tree_id + 1) * (instance.num_leaves - 1) + 2;
let normalized_tree =
build_normalized_tree(&mut tree_builder, tree, NodeIdx(root_id as u32));

println!("{}", normalized_tree.top_down().to_newick_string());
}
}

fn build_normalized_tree(
builder: &mut Builder,
node: impl TopDownCursor,
node_id: NodeIdx,
) -> Node {
let root = build_normalized_tree_rec(builder, node, node_id).0;
builder.make_root(root)
}

fn build_normalized_tree_rec(
builder: &mut Builder,
node: impl TopDownCursor,
node_id: NodeIdx,
) -> (Node, Label, NodeIdx) {
match node.visit() {
// Base case: For a leaf with simply copy the label and build a new leaf node
NodeType::Leaf(label) => (builder.new_leaf(label), label, node_id),

// Recursion into subtrees:
NodeType::Inner(left, right) => {
// recursively decent into both subtrees
let (child0, label0, next_node_id) =
build_normalized_tree_rec(builder, left, node_id.incremented());
let (child1, label1, next_node_id) =
build_normalized_tree_rec(builder, right, next_node_id);

// construct a new inner node with the smaller subtree to the left
if label0 < label1 {
(
builder.new_inner(node_id, child0, child1),
label0,
next_node_id,
)
} else {
(
builder.new_inner(node_id, child1, child0),
label1,
next_node_id,
)
}
}
}
}
7 changes: 7 additions & 0 deletions examples/tiny01.nw
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#s hash "0x118494c254c36869026cfc"
#s name "tiny01"
#s desc "Example shown on https://pacechallenge.org"
#p 2 6
(((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]]]
41 changes: 39 additions & 2 deletions src/binary_tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub use indexed_bin_tree::*;
pub mod depth_first_search;
pub use depth_first_search::DepthFirstSearch;

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct NodeIdx(pub u32);

impl NodeIdx {
Expand All @@ -28,11 +28,13 @@ impl From<Label> for NodeIdx {
}
}

/// Generic interface to build binary trees required by Newick parser.
pub trait TreeBuilder {
type Node;

/// Creates a new inner node with the two children provided.
///
///
/// # Example
/// ```
/// use pace26io::binary_tree::*;
Expand Down Expand Up @@ -82,9 +84,18 @@ pub trait TreeBuilder {
}
}

pub enum NodeType<T> {
Inner(T, T),
Leaf(Label),
}

/// Minimal requirement for a binary tree implemenation (e.g., used for Newick writer).
pub trait TopDownCursor: Sized {
/// Returns the children iff self is an inner node and `None` otherwise.
///
/// # Remark
/// When traversing a tree, it is often more convenient to use [`TopDownCursor::visit`].
///
/// # Example
/// ```
/// use pace26io::binary_tree::*;
Expand Down Expand Up @@ -137,6 +148,9 @@ pub trait TopDownCursor: Sized {

/// Returns the label iff self is a leaf node and `None` otherwise.
///
/// # Remark
/// When traversing a tree, it is often more convenient to use [`TopDownCursor::visit`].
///
/// # Example
/// ```
/// use pace26io::binary_tree::*;
Expand All @@ -147,7 +161,7 @@ pub trait TopDownCursor: Sized {
///
/// assert_eq!(leaf.top_down().leaf_label().unwrap(), Label(1337));
/// assert!( root.top_down().leaf_label().is_none());
///
/// ```
fn leaf_label(&self) -> Option<Label>;

/// Returns true iff self is an inner node
Expand Down Expand Up @@ -183,6 +197,29 @@ pub trait TopDownCursor: Sized {
fn is_leaf(&self) -> bool {
self.leaf_label().is_some()
}

/// For an inner node returns NodeType::Inner, for a leaf returns NodeType::Leaf.
///
/// # Example
/// ```
/// use pace26io::binary_tree::*;
///
/// let mut builder = BinTreeBuilder::default();
/// let leaf = builder.new_leaf(Label(1));
/// let root = builder.new_inner(NodeIdx::new(0), leaf.clone(), leaf.clone());
///
/// assert!(matches!(root.top_down().visit(), NodeType::Inner(..)));
/// assert!(matches!(leaf.top_down().visit(), NodeType::Leaf(..)));
/// ```
fn visit(&self) -> NodeType<Self> {
if let Some((l, r)) = self.children() {
NodeType::Inner(l, r)
} else if let Some(l) = self.leaf_label() {
NodeType::Leaf(l)
} else {
unreachable!("Each node is either an inner node or a leaf");
}
}
}

/// Tree with indexed inner nodes
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]

pub mod binary_tree;
pub mod newick;
pub mod pace;
21 changes: 11 additions & 10 deletions src/newick/binary_tree_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ use std::io::Write;

impl<B: TopDownCursor> NewickWriter for B {
fn write_newick_inner(&self, writer: &mut impl Write) -> std::io::Result<()> {
if let Some((left, right)) = self.children() {
write!(writer, "(")?;
left.write_newick_inner(writer)?;
write!(writer, ",")?;
right.write_newick_inner(writer)?;
write!(writer, ")")
} else if let Some(Label(label)) = self.leaf_label() {
write!(writer, "{label}")
} else {
unreachable!("Nodes must either have children or a label; this one has neither");
match self.visit() {
NodeType::Inner(left, right) => {
write!(writer, "(")?;
left.write_newick_inner(writer)?;
write!(writer, ",")?;
right.write_newick_inner(writer)?;
write!(writer, ")")
}
NodeType::Leaf(Label(label)) => {
write!(writer, "{label}")
}
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/pace/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod parameters;
pub mod reader;
pub mod simplified;
22 changes: 11 additions & 11 deletions src/pace/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub struct InstanceReader<'a, V: InstanceVisitor> {
///
/// Example: see the documentation of [`InstanceReader`].
pub trait InstanceVisitor {
fn visit_header(&mut self, _lineno: usize, _num_trees: usize, _num_leafs: usize) -> Action {
fn visit_header(&mut self, _lineno: usize, _num_trees: usize, _num_leaves: usize) -> Action {
Action::Continue
}
fn visit_tree(&mut self, _lineno: usize, _line: &str) -> Action {
Expand All @@ -55,7 +55,7 @@ pub trait InstanceVisitor {
fn visit_line_with_extra_whitespace(&mut self, _lineno: usize, _line: &str) -> Action {
Action::Continue
}
fn visit_unrecognized_dash_line(&mut self, _lineno: usize, _line: &str) -> Action {
fn visit_unrecognized_hash_line(&mut self, _lineno: usize, _line: &str) -> Action {
Action::Continue
}
fn visit_unrecognized_line(&mut self, _lineno: usize, _line: &str) -> Action {
Expand Down Expand Up @@ -221,7 +221,7 @@ impl<'a, V: InstanceVisitor> InstanceReader<'a, V> {
}
} else {
// unrecognized line
visit!(visit_unrecognized_dash_line, lineno, content);
visit!(visit_unrecognized_hash_line, lineno, content);
}
continue;
}
Expand All @@ -247,15 +247,15 @@ mod tests {
pub headers: Vec<(usize, usize, usize)>,
pub trees: Vec<(usize, String)>,
pub extra_whitespace_lines: Vec<(usize, String)>,
pub unrecognized_dash_lines: Vec<(usize, String)>,
pub unrecognized_hash_lines: Vec<(usize, String)>,
pub unrecognized_lines: Vec<(usize, String)>,
pub stride_lines: Vec<(usize, String, String, String)>,
pub param_tree_decomp: Option<(usize, TreeDecomposition)>,
}

impl InstanceVisitor for TestVisitor {
fn visit_header(&mut self, lineno: usize, num_trees: usize, num_leafs: usize) -> Action {
self.headers.push((lineno, num_trees, num_leafs));
fn visit_header(&mut self, lineno: usize, num_trees: usize, num_leaves: usize) -> Action {
self.headers.push((lineno, num_trees, num_leaves));
Action::Continue
}

Expand All @@ -269,8 +269,8 @@ mod tests {
Action::Continue
}

fn visit_unrecognized_dash_line(&mut self, lineno: usize, line: &str) -> Action {
self.unrecognized_dash_lines
fn visit_unrecognized_hash_line(&mut self, lineno: usize, line: &str) -> Action {
self.unrecognized_hash_lines
.push((lineno, line.to_string()));
Action::Continue
}
Expand Down Expand Up @@ -318,7 +318,7 @@ mod tests {
vec![(1, "(1);".to_string()), (3, "(2);".to_string())]
);
assert!(visitor.extra_whitespace_lines.is_empty());
assert!(visitor.unrecognized_dash_lines.is_empty());
assert!(visitor.unrecognized_hash_lines.is_empty());
assert!(visitor.unrecognized_lines.is_empty());
}

Expand All @@ -339,7 +339,7 @@ mod tests {
visitor.extra_whitespace_lines,
vec![(1, " (1);".to_string())]
);
assert!(visitor.unrecognized_dash_lines.is_empty());
assert!(visitor.unrecognized_hash_lines.is_empty());
assert!(visitor.unrecognized_lines.is_empty());
}

Expand All @@ -361,7 +361,7 @@ mod tests {
vec![(1, " (1);".to_string())]
);
assert_eq!(
visitor.unrecognized_dash_lines,
visitor.unrecognized_hash_lines,
vec![(4, "#<illegal comment".to_string())]
);
assert_eq!(
Expand Down
Loading