use smtkit_core::{Ctx, Op, Sort, TermId};
use structops::soft_shortest_path::{soft_shortest_path_edge_marginals, Edge};
const CANDIDATE_EDGES: &[(usize, usize, f64, i64)] = &[
(0, 1, 3.0, 20), (0, 2, 5.0, 15), (1, 3, 2.0, 10), (2, 3, 6.0, 5), (2, 4, 8.0, 3), (3, 5, 4.0, 12), (4, 5, 3.0, 10), ];
const REQUIRED_FLOW: i64 = 5;
const BUDGET: i64 = 15;
fn build_constraints() -> (Ctx, TermId, Vec<TermId>, Vec<String>) {
let mut ctx = Ctx::new();
let mut conjuncts: Vec<TermId> = Vec::new();
let mut flow_vars: Vec<TermId> = Vec::new();
let mut edge_labels: Vec<String> = Vec::new();
for (i, &(from, to, _cost, cap)) in CANDIDATE_EDGES.iter().enumerate() {
let name = format!("flow_{from}_{to}");
edge_labels.push(format!("{from}->{to}"));
let fv = ctx.var(name.as_str(), Sort::Int);
flow_vars.push(fv);
let zero = ctx.int_lit(0);
let lb = ctx.app(Op::Le, vec![zero, fv]).expect("type-safe");
conjuncts.push(lb);
let cap_lit = ctx.int_lit(cap);
let ub = ctx.app(Op::Le, vec![fv, cap_lit]).expect("type-safe");
conjuncts.push(ub);
let _ = i; }
let intermediate_nodes = [1usize, 2, 3, 4];
for &node in &intermediate_nodes {
let mut incoming = Vec::new();
let mut outgoing = Vec::new();
for (i, &(from, to, _, _)) in CANDIDATE_EDGES.iter().enumerate() {
if to == node {
incoming.push(flow_vars[i]);
}
if from == node {
outgoing.push(flow_vars[i]);
}
}
let in_sum = if incoming.len() >= 2 {
ctx.app(Op::Add, incoming).expect("type-safe")
} else if incoming.len() == 1 {
incoming[0]
} else {
ctx.int_lit(0)
};
let out_sum = if outgoing.len() >= 2 {
ctx.app(Op::Add, outgoing).expect("type-safe")
} else if outgoing.len() == 1 {
outgoing[0]
} else {
ctx.int_lit(0)
};
let balance = ctx.app(Op::Eq, vec![in_sum, out_sum]).expect("type-safe");
conjuncts.push(balance);
}
let mut source_out = Vec::new();
for (i, &(from, _, _, _)) in CANDIDATE_EDGES.iter().enumerate() {
if from == 0 {
source_out.push(flow_vars[i]);
}
}
let source_sum = ctx.app(Op::Add, source_out).expect("type-safe");
let req = ctx.int_lit(REQUIRED_FLOW);
let source_eq = ctx.app(Op::Eq, vec![source_sum, req]).expect("type-safe");
conjuncts.push(source_eq);
let budget_lit = ctx.int_lit(BUDGET);
let mut cost_terms = Vec::new();
for (i, &(_, _, cost, _)) in CANDIDATE_EDGES.iter().enumerate() {
let zero = ctx.int_lit(0);
let cost_lit = ctx.int_lit(cost as i64);
let flow_pos = ctx
.app(Op::Lt, vec![zero, flow_vars[i]])
.expect("type-safe");
let edge_cost = ctx
.app(Op::Ite, vec![flow_pos, cost_lit, zero])
.expect("type-safe");
cost_terms.push(edge_cost);
}
let total_cost = ctx.app(Op::Add, cost_terms).expect("type-safe");
let budget_cstr = ctx
.app(Op::Le, vec![total_cost, budget_lit])
.expect("type-safe");
conjuncts.push(budget_cstr);
let top = ctx.app(Op::And, conjuncts).expect("type-safe");
(ctx, top, flow_vars, edge_labels)
}
fn feasible_edges() -> Vec<Edge> {
CANDIDATE_EDGES
.iter()
.filter(|&&(from, to, cost, cap)| {
let dominated = from == 2 && to == 4;
let _ = (cost, cap); !dominated
})
.map(|&(from, to, cost, _cap)| Edge { from, to, cost })
.collect()
}
fn main() {
let (ctx, top, _flow_vars, edge_labels) = build_constraints();
println!("=== Phase 1: Hard constraints (smtkit-core) ===");
println!();
println!("Candidate edges:");
for (i, &(from, to, cost, cap)) in CANDIDATE_EDGES.iter().enumerate() {
println!(
" [{i}] {from}->{to} cost={cost:.0} capacity={cap} label={}",
edge_labels[i]
);
}
println!();
let vars = ctx.free_vars(top);
println!(
"Constraint variables ({} total): {}",
vars.len(),
vars.iter()
.map(|s| s.0.as_str())
.collect::<Vec<_>>()
.join(", ")
);
println!();
println!(
"Constraints encode: capacity bounds on {} edges, flow balance on 4 intermediate nodes,",
CANDIDATE_EDGES.len()
);
println!(" source-flow = {REQUIRED_FLOW}, budget <= {BUDGET}.");
println!();
println!(
"Edge 2->4 (cost=8, cap=3) is infeasible: any path through it costs >= 16 > {BUDGET}.",
);
println!();
let edges = feasible_edges();
let n = 6;
println!("=== Phase 2: Soft shortest-path marginals (structops) ===");
println!();
println!(
"Feasible edges ({} of {} candidates):",
edges.len(),
CANDIDATE_EDGES.len()
);
for (i, e) in edges.iter().enumerate() {
println!(" [{i}] {}->{} cost={:.1}", e.from, e.to, e.cost);
}
println!();
println!("Feasible source-to-sink paths (0 -> 5):");
println!(" 0->1->3->5 cost = 3+2+4 = 9 <-- shortest");
println!(" 0->2->3->5 cost = 5+6+4 = 15");
println!(" (0->2->4->5 is pruned: edge 2->4 infeasible)");
println!();
let gammas = [0.1, 1.0, 10.0];
print!("{:>16}", "edge \\ gamma");
for &g in &gammas {
print!(" {:>10}", format!("{g}"));
}
println!();
print!("{:>16}", "");
for _ in &gammas {
print!(" ----------");
}
println!();
let results: Vec<(f64, Vec<f64>)> = gammas
.iter()
.map(|&g| {
soft_shortest_path_edge_marginals(n, &edges, g)
.expect("computation should succeed on feasible graph")
})
.collect();
for (i, e) in edges.iter().enumerate() {
let label = format!("{}->{} (c={:.0})", e.from, e.to, e.cost);
print!("{:>16}", label);
for (_value, marginals) in &results {
print!(" {:>10.6}", marginals[i]);
}
println!();
}
println!();
print!("{:>16}", "soft value");
for (value, _) in &results {
print!(" {:>10.4}", value);
}
println!();
println!();
println!("Interpretation:");
println!(
" gamma=0.1: marginals concentrate on the shortest feasible path (0->1->3->5, cost=9)."
);
println!(
" Edges 0->1, 1->3, 3->5 each get marginal ~1.0; the 0->2->3->5 path gets ~0."
);
println!(" gamma=1.0: some mass shifts to the longer path (0->2->3->5, cost=15).");
println!(" gamma=10.0: marginals spread toward uniform over both paths.");
println!();
println!(
"The hard constraints (expressed in smtkit-core) determine *which* edges are feasible."
);
println!("The soft optimization (structops) determines *how important* each feasible edge is");
println!("under the Gibbs distribution over paths, parameterized by temperature gamma.");
}