#![allow(clippy::expect_used)]
use std::time::{Duration, Instant};
use panproto_mig::DEFAULT_WEIGHTS;
use panproto_mig::hom_search::{DomainConstraints, SearchOptions};
use panproto_mig::solve::build::{NoEvidence, build_cfn};
use panproto_mig::solve::{
Cfn, DEFAULT_MEM_BYTES, LimitKind, SearchBudget, SearchWarning, SolverPath, choose_order,
elimination_cost, solve,
};
use panproto_protocols::raw_file;
use panproto_schema::{EdgeRule, Protocol, Schema, SchemaBuilder};
const LIVENESS: Duration = Duration::from_secs(60);
fn protocol() -> Protocol {
Protocol {
name: "fallback".to_owned(),
schema_theory: "ThTest".to_owned(),
instance_theory: "ThWType".to_owned(),
edge_rules: vec![EdgeRule {
edge_kind: "prop".to_owned(),
src_kinds: vec!["object".to_owned()],
tgt_kinds: vec!["object".to_owned()],
}],
obj_kinds: vec!["object".to_owned()],
constraint_sorts: vec![],
..Protocol::default()
}
}
fn clique(k: usize) -> Schema {
let mut builder = SchemaBuilder::new(&protocol());
for index in 0..k {
builder = builder
.vertex(&format!("o{index}"), "object", None::<&str>)
.expect("vertex");
}
for left in 0..k {
for right in 0..k {
if left != right {
builder = builder
.edge(
&format!("o{left}"),
&format!("o{right}"),
"prop",
Some(&format!("p{right}")),
)
.expect("edge");
}
}
}
builder.entry("o0").build().expect("build")
}
fn network(schema: &Schema) -> Cfn {
build_cfn(
schema,
schema,
&SearchOptions::default(),
&DomainConstraints::default(),
&NoEvidence,
DEFAULT_WEIGHTS,
DEFAULT_MEM_BYTES,
)
.expect("the fixture poses")
}
fn file_network(lines: usize) -> Cfn {
use std::fmt::Write as _;
let text = (0..lines).fold(String::new(), |mut out, index| {
let _ = writeln!(out, "line {index} of the file");
out
});
let parsed = raw_file::parse_text(&text, "sample.txt").expect("parse");
build_cfn(
&parsed,
&parsed,
&SearchOptions::default(),
&DomainConstraints::default(),
&NoEvidence,
DEFAULT_WEIGHTS,
DEFAULT_MEM_BYTES,
)
.expect("a line-per-vertex parse poses")
}
#[test]
fn a_search_past_the_budget_stops_and_says_so() {
let cfn = network(&clique(10));
let (order, width) = choose_order(&cfn);
let refused = elimination_cost(&cfn, &order);
let budget = SearchBudget::default().with_op_budget(1_000_000);
assert!(
!refused.fits(&budget),
"the fixture is meant to be past the budget"
);
let started = Instant::now();
let found = solve(&cfn, &budget);
let elapsed = started.elapsed();
assert!(matches!(found.path, SolverPath::BranchAndBound { .. }));
assert_eq!(found.limit_hit, Some(LimitKind::Operations));
assert!(!found.proven_optimal, "a stopped search proves nothing");
assert!(
elapsed < LIVENESS,
"the fallback took {elapsed:?}, which is not a bounded search"
);
let named = found.warnings.iter().find_map(|warning| match warning {
SearchWarning::EliminationOutOfBudget {
width: reported,
entries,
operations,
} => Some((*reported, *entries, *operations)),
_ => None,
});
assert_eq!(named, Some((width, refused.entries, refused.operations)));
assert!(refused.operations > budget.op_budget);
}
#[test]
fn a_larger_budget_gets_further() {
let cfn = network(&clique(10));
let tight = solve(&cfn, &SearchBudget::default().with_op_budget(10_000));
let loose = solve(&cfn, &SearchBudget::default().with_op_budget(10_000_000));
assert_eq!(tight.limit_hit, Some(LimitKind::Operations));
assert!(loose.nodes >= tight.nodes);
assert!(
loose.limit_hit.is_none() || loose.nodes > tight.nodes,
"a budget ten thousand times larger neither finished nor got further"
);
}
#[test]
fn the_same_search_finishes_when_the_budget_allows() {
let cfn = network(&clique(10));
let found = solve(&cfn, &SearchBudget::default());
assert!(matches!(found.path, SolverPath::BranchAndBound { .. }));
assert_eq!(found.limit_hit, None);
assert!(found.proven_optimal);
assert!(found.best.is_some());
}
#[test]
fn the_same_network_stops_in_the_same_place() {
let cfn = network(&clique(10));
let budget = SearchBudget::default().with_op_budget(2_000_000);
let first = solve(&cfn, &budget);
for _ in 0..2 {
let again = solve(&cfn, &budget);
assert_eq!(first.nodes, again.nodes);
assert_eq!(first.limit_hit, again.limit_hit);
assert_eq!(first.best, again.best);
assert_eq!(first.lower_bound, again.lower_bound);
assert_eq!(first.upper_bound, again.upper_bound);
}
assert_eq!(first.limit_hit, Some(LimitKind::Operations));
}
#[test]
fn the_star_that_used_to_hang_now_returns() {
let cfn = file_network(200);
let (order, _) = choose_order(&cfn);
let refused = elimination_cost(&cfn, &order);
let budget = SearchBudget::default().with_op_budget(refused.operations / 2);
let started = Instant::now();
let found = solve(&cfn, &budget);
let elapsed = started.elapsed();
assert!(matches!(found.path, SolverPath::BranchAndBound { .. }));
assert_eq!(found.limit_hit, Some(LimitKind::Operations));
assert!(
elapsed < LIVENESS,
"the star took {elapsed:?}, which is the failure this test exists for"
);
let whole = solve(&cfn, &SearchBudget::default());
assert!(matches!(whole.path, SolverPath::Eliminate { width: 1 }));
assert!(whole.proven_optimal);
}