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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ serde_json = "1.0"
thiserror = "2.0"
num-bigint = "0.4"
num-traits = "0.2"
good_lp = { version = "1.8", default-features = false, optional = true }
good_lp = { version = "=1.14.2", default-features = false, optional = true }
inventory = "0.3"
ordered-float = "5.0"
rand = "0.10"
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ problemreductions::inventory::submit! {
}),
capabilities: EdgeCapabilities::aggregate_only(),
overhead_eval_fn: |_| ProblemSize::new(vec![]),
source_size_fn: |_| ProblemSize::new(vec![]),
}
}

Expand All @@ -202,6 +203,7 @@ problemreductions::inventory::submit! {
}),
capabilities: EdgeCapabilities::aggregate_only(),
overhead_eval_fn: |_| ProblemSize::new(vec![]),
source_size_fn: |_| ProblemSize::new(vec![]),
}
}

Expand Down
56 changes: 52 additions & 4 deletions problemreductions-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,47 @@ fn generate_overhead_eval_fn(
})
}

/// Generate a function that extracts the source problem's size fields from `&dyn Any`.
///
/// Collects all variable names referenced in the overhead expressions, generates
/// getter calls for each, and returns a `ProblemSize`.
fn generate_source_size_fn(
fields: &[(String, String)],
source_type: &Type,
) -> syn::Result<TokenStream2> {
let src_ident = syn::Ident::new("__src", proc_macro2::Span::call_site());

// Collect all unique variable names from overhead expressions
let mut var_names = std::collections::BTreeSet::new();
for (_, expr_str) in fields {
let parsed = parser::parse_expr(expr_str).map_err(|e| {
syn::Error::new(
proc_macro2::Span::call_site(),
format!("error parsing overhead expression \"{expr_str}\": {e}"),
)
})?;
for v in parsed.variables() {
var_names.insert(v.to_string());
}
}

let getter_tokens: Vec<_> = var_names
.iter()
.map(|var| {
let getter = syn::Ident::new(var, proc_macro2::Span::call_site());
let name_lit = var.as_str();
quote! { (#name_lit, #src_ident.#getter() as usize) }
})
.collect();

Ok(quote! {
|__any_src: &dyn std::any::Any| -> crate::types::ProblemSize {
let #src_ident = __any_src.downcast_ref::<#source_type>().unwrap();
crate::types::ProblemSize::new(vec![#(#getter_tokens),*])
}
})
}

/// Generate the reduction entry code
fn generate_reduction_entry(
attrs: &ReductionAttrs,
Expand Down Expand Up @@ -288,21 +329,27 @@ fn generate_reduction_entry(
let source_variant_body = make_variant_fn_body(source_type, &type_generics)?;
let target_variant_body = make_variant_fn_body(&target_type, &type_generics)?;

// Generate overhead and eval fn
let (overhead, overhead_eval_fn) = match &attrs.overhead {
// Generate overhead, eval fn, and source size fn
let (overhead, overhead_eval_fn, source_size_fn) = match &attrs.overhead {
Some(OverheadSpec::Legacy(tokens)) => {
let eval_fn = quote! {
|_: &dyn std::any::Any| -> crate::types::ProblemSize {
panic!("overhead_eval_fn not available for legacy overhead syntax; \
migrate to parsed syntax: field = \"expression\"")
}
};
(tokens.clone(), eval_fn)
let size_fn = quote! {
|_: &dyn std::any::Any| -> crate::types::ProblemSize {
crate::types::ProblemSize::new(vec![])
}
};
(tokens.clone(), eval_fn, size_fn)
}
Some(OverheadSpec::Parsed(fields)) => {
let overhead_tokens = generate_parsed_overhead(fields)?;
let eval_fn = generate_overhead_eval_fn(fields, source_type)?;
(overhead_tokens, eval_fn)
let size_fn = generate_source_size_fn(fields, source_type)?;
(overhead_tokens, eval_fn, size_fn)
}
None => {
return Err(syn::Error::new(
Expand Down Expand Up @@ -337,6 +384,7 @@ fn generate_reduction_entry(
reduce_aggregate_fn: None,
capabilities: #capabilities,
overhead_eval_fn: #overhead_eval_fn,
source_size_fn: #source_size_fn,
}
}

Expand Down
33 changes: 33 additions & 0 deletions src/rules/cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,39 @@ impl PathCostFn for MinimizeSteps {
}
}

/// Minimize total output size (sum of all output field values).
///
/// Prefers reduction paths that produce smaller intermediate and final problems.
/// Breaks ties that `MinimizeSteps` cannot resolve (e.g., two 2-step paths
/// where one produces 144 ILP variables and the other 1,332).
pub struct MinimizeOutputSize;

impl PathCostFn for MinimizeOutputSize {
fn edge_cost(&self, overhead: &ReductionOverhead, size: &ProblemSize) -> f64 {
let output = overhead.evaluate_output_size(size);
output.total() as f64
}
}

/// Minimize steps first, then use output size as tiebreaker.
///
/// Each edge has a primary cost of `STEP_WEIGHT` (ensuring fewer-step paths
/// always win) plus a small overhead-based cost that breaks ties between
/// equal-step paths.
pub struct MinimizeStepsThenOverhead;

impl PathCostFn for MinimizeStepsThenOverhead {
fn edge_cost(&self, overhead: &ReductionOverhead, size: &ProblemSize) -> f64 {
// Use a large step weight to ensure step count dominates.
// The overhead tiebreaker uses log1p to compress the range,
// keeping it far smaller than STEP_WEIGHT for any realistic problem size.
const STEP_WEIGHT: f64 = 1e9;
let output = overhead.evaluate_output_size(size);
let overhead_tiebreaker = (1.0 + output.total() as f64).ln();
STEP_WEIGHT + overhead_tiebreaker
}
}

/// Custom cost function from closure.
pub struct CustomCost<F>(pub F);

Expand Down
48 changes: 48 additions & 0 deletions src/rules/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,54 @@ impl ReductionGraph {
result
}

/// Evaluate the cumulative output size along a reduction path.
///
/// Walks the path from start to end, applying each edge's overhead
/// expressions to transform the problem size at each step.
/// Returns `None` if any edge in the path cannot be found.
pub fn evaluate_path_overhead(
&self,
path: &ReductionPath,
input_size: &ProblemSize,
) -> Option<ProblemSize> {
let mut current_size = input_size.clone();
for pair in path.steps.windows(2) {
let src = self.lookup_node(&pair[0].name, &pair[0].variant)?;
let dst = self.lookup_node(&pair[1].name, &pair[1].variant)?;
let edge_idx = self.graph.find_edge(src, dst)?;
let edge = &self.graph[edge_idx];
current_size = edge.overhead.evaluate_output_size(&current_size);
}
Some(current_size)
}

/// Compute the source problem's size from a type-erased instance.
///
/// Iterates over all registered reduction entries with a matching source name
/// and merges their `source_size_fn` results to capture all size fields.
/// Different entries may reference different getter methods (e.g., one uses
/// `num_vertices` while another also uses `num_edges`).
pub fn compute_source_size(name: &str, instance: &dyn Any) -> ProblemSize {
let mut merged: Vec<(String, usize)> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();

for entry in inventory::iter::<ReductionEntry> {
if entry.source_name == name {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(entry.source_size_fn)(instance)
}));
if let Ok(size) = result {
for (k, v) in size.components {
if seen.insert(k.clone()) {
merged.push((k, v));
}
}
}
}
}
ProblemSize { components: merged }
}

/// Get all incoming reductions to a problem (across all its variants).
pub fn incoming_reductions(&self, name: &str) -> Vec<ReductionEdgeInfo> {
let Some(indices) = self.name_to_nodes.get(name) else {
Expand Down
167 changes: 167 additions & 0 deletions src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! Reduction from HamiltonianCircuit to BiconnectivityAugmentation.
//!
//! Based on the Eswaran & Tarjan (1976) approach:
//!
//! Given a Hamiltonian Circuit instance G = (V, E) with n vertices, construct a
//! BiconnectivityAugmentation instance as follows:
//!
//! 1. Start with an edgeless graph on n vertices.
//! 2. For each pair (u, v) with u < v, create a potential edge with:
//! - weight 1 if {u, v} is in E
//! - weight 2 if {u, v} is not in E
//! 3. Set budget B = n.
//!
//! G has a Hamiltonian circuit iff there exists a biconnectivity augmentation of
//! cost exactly n using only weight-1 edges (i.e., original edges).
//!
//! The selected weight-1 edges form a Hamiltonian cycle in G, which is necessarily
//! biconnected. Any augmentation using a weight-2 edge would cost at least n+1,
//! exceeding the budget of n (since at least n edges are needed for biconnectivity).

use crate::models::graph::{BiconnectivityAugmentation, HamiltonianCircuit};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::topology::{Graph, SimpleGraph};

/// Result of reducing HamiltonianCircuit to BiconnectivityAugmentation.
///
/// Stores the target problem and the mapping from potential edge indices to
/// vertex pairs for solution extraction.
#[derive(Debug, Clone)]
pub struct ReductionHamiltonianCircuitToBiconnectivityAugmentation {
target: BiconnectivityAugmentation<SimpleGraph, i32>,
/// Number of vertices in the original graph.
num_vertices: usize,
/// Potential edges as (u, v) pairs, in the same order as the target's potential_weights.
potential_edges: Vec<(usize, usize)>,
}

impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation {
type Source = HamiltonianCircuit<SimpleGraph>;
type Target = BiconnectivityAugmentation<SimpleGraph, i32>;

fn target_problem(&self) -> &Self::Target {
&self.target
}

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
let n = self.num_vertices;
if n < 3 {
return vec![0; n];
}

// Collect selected edges (those with config value 1)
let mut adj: Vec<Vec<usize>> = vec![vec![]; n];
for (i, &(u, v)) in self.potential_edges.iter().enumerate() {
if i < target_solution.len() && target_solution[i] == 1 {
adj[u].push(v);
adj[v].push(u);
}
}

// Check that every vertex has exactly degree 2 (Hamiltonian cycle)
if adj.iter().any(|neighbors| neighbors.len() != 2) {
return vec![0; n];
}

// Walk the cycle starting from vertex 0
let mut circuit = Vec::with_capacity(n);
circuit.push(0);
let mut prev = 0;
let mut current = adj[0][0];
while current != 0 {
circuit.push(current);
let next = if adj[current][0] == prev {
adj[current][1]
} else {
adj[current][0]
};
prev = current;
current = next;

// Safety: if we've visited more than n vertices, something is wrong
if circuit.len() > n {
return vec![0; n];
}
}

if circuit.len() == n {
circuit
} else {
vec![0; n]
}
}
}

#[reduction(
overhead = {
num_vertices = "num_vertices",
num_edges = "0",
num_potential_edges = "num_vertices * (num_vertices - 1) / 2",
}
)]
impl ReduceTo<BiconnectivityAugmentation<SimpleGraph, i32>> for HamiltonianCircuit<SimpleGraph> {
type Result = ReductionHamiltonianCircuitToBiconnectivityAugmentation;

fn reduce_to(&self) -> Self::Result {
let n = self.num_vertices();
let graph = self.graph();

// Edgeless initial graph
let initial_graph = SimpleGraph::empty(n);

// Create potential edges for all pairs (u, v) with u < v
let mut potential_weights = Vec::new();
let mut potential_edges = Vec::new();
for u in 0..n {
for v in (u + 1)..n {
let weight = if graph.has_edge(u, v) { 1 } else { 2 };
potential_weights.push((u, v, weight));
potential_edges.push((u, v));
}
}

// Budget = n (exactly enough for n weight-1 edges)
let budget = n as i32;

let target = BiconnectivityAugmentation::new(initial_graph, potential_weights, budget);

ReductionHamiltonianCircuitToBiconnectivityAugmentation {
target,
num_vertices: n,
potential_edges,
}
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
use crate::export::SolutionPair;

vec![crate::example_db::specs::RuleExampleSpec {
id: "hamiltoniancircuit_to_biconnectivityaugmentation",
build: || {
// Square graph (4-cycle): 0-1-2-3-0
let source = HamiltonianCircuit::new(SimpleGraph::cycle(4));
// Potential edges for 4 vertices (indices 0..5):
// 0: (0,1) w=1, 1: (0,2) w=2, 2: (0,3) w=1,
// 3: (1,2) w=1, 4: (1,3) w=2, 5: (2,3) w=1
// HC 0-1-2-3-0 selects edges (0,1),(1,2),(2,3),(0,3) => indices 0,3,5,2
// Config: [1, 0, 1, 1, 0, 1]
crate::example_db::specs::rule_example_with_witness::<
_,
BiconnectivityAugmentation<SimpleGraph, i32>,
>(
source,
SolutionPair {
source_config: vec![0, 1, 2, 3],
target_config: vec![1, 0, 1, 1, 0, 1],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs"]
mod tests;
Loading
Loading