use crate::ground::{ground, Outcome};
use crate::hash::FxHashSet;
use crate::packed::PackedTask;
use crate::partition::{interaction_partition, merge_at, merge_with_neighbor, Subgoal};
use crate::temporal::{
self, build_kind, solve_from, treplay, validate, Kind, TimedPlan, TimedStep,
};
use crate::types::{Domain, Problem};
const EPS: f64 = 0.001;
pub(crate) struct ContractRec {
pub goal: String,
pub plan: TimedPlan,
pub offset: f64,
}
pub(crate) struct Decomp {
pub contracts: Vec<ContractRec>,
pub plan: TimedPlan,
pub monolithic: bool,
}
fn render_subgoal(task: &PackedTask, g: &crate::partition::Subgoal) -> String {
let mut parts: Vec<String> = g
.pos
.iter()
.map(|&f| task.fact_names[f as usize].clone())
.collect();
for np in &g.num {
parts.push(render_numpre(task, np));
}
if parts.is_empty() {
"(empty)".to_string()
} else {
parts.join(", ")
}
}
fn render_numpre(task: &PackedTask, np: &crate::types::NumPre) -> String {
use crate::types::{CompOp, NExpr};
let op = match np.op {
CompOp::Lt => "<",
CompOp::Le => "<=",
CompOp::Eq => "=",
CompOp::Ge => ">=",
CompOp::Gt => ">",
};
match (&np.lhs, &np.rhs) {
(NExpr::Fluent(t), NExpr::Num(v)) => {
let name = task
.fluent_names
.get(*t as usize)
.cloned()
.unwrap_or_else(|| format!("fluent#{t}"));
format!("{name} {op} {v}")
}
_ => format!("{:?} {op} {:?}", np.lhs, np.rhs),
}
}
fn monolithic_decomp(goal: String, plan: TimedPlan) -> Decomp {
Decomp {
contracts: vec![ContractRec {
goal,
offset: 0.0,
plan: plan.clone(),
}],
plan,
monolithic: true,
}
}
pub fn solve(domain: &Domain, problem: &Problem, threads: usize) -> Option<TimedPlan> {
decompose(domain, problem, threads).map(|d| d.plan)
}
pub(crate) fn solve_after_ladder(
domain: &Domain,
problem: &Problem,
threads: usize,
tier: crate::features::DemandMode,
) -> Option<TimedPlan> {
decompose_inner(domain, problem, threads, tier, false).map(|d| d.plan)
}
pub(crate) fn decompose(domain: &Domain, problem: &Problem, threads: usize) -> Option<Decomp> {
let tier = crate::features::demand_mode();
decompose_inner(domain, problem, threads, tier, true)
}
fn decompose_inner(
domain: &Domain,
problem: &Problem,
threads: usize,
tier: crate::features::DemandMode,
monolithic_fallback: bool,
) -> Option<Decomp> {
let c = temporal::compile(domain, problem);
let task = match ground(&c.domain, &c.problem, threads) {
Outcome::Task(t) => t,
Outcome::GoalTrue => {
let empty = TimedPlan {
steps: Vec::new(),
makespan: 0.0,
};
return Some(monolithic_decomp("(goal already satisfied)".into(), empty));
}
_ => return None,
};
if temporal::statically_unsolvable(&task, &task.initial(), &task.goal_pos, &task.goal_num) {
return None;
}
let kind = build_kind(&task, &c);
let whole_goal = render_subgoal(
&task,
&crate::partition::Subgoal {
pos: task.goal_pos.clone(),
num: task.goal_num.clone(),
},
);
let mutex = crate::invariants::synthesize(domain, &task);
let mut groups = partition_temporal(&task, &kind, &mutex);
let init = task.initial();
let dbg = std::env::var("FF_RES_DEBUG").is_ok();
if dbg {
eprintln!("[TDECOMP] {} initial contracts", groups.len());
}
loop {
if groups.len() == 1 {
if !monolithic_fallback {
return None;
}
return temporal::solve(domain, problem, threads)
.map(|p| monolithic_decomp(whole_goal.clone(), p));
}
let mut state = init.clone();
let mut offset = 0.0_f64;
let mut composed: Vec<TimedStep> = Vec::new();
let mut done = vec![false; groups.len()];
let mut conflict: Option<(usize, Option<usize>)> = None;
let mut record: Vec<ContractRec> = Vec::new();
for i in 0..groups.len() {
if groups[i].is_empty() || task.goal_met_with(&state, &groups[i].pos, &groups[i].num) {
done[i] = true;
continue;
}
let protected: FxHashSet<u32> = (0..i)
.filter(|&j| done[j])
.flat_map(|j| groups[j].pos.iter().copied())
.collect();
let forbidden: Vec<bool> = if protected.is_empty() {
Vec::new()
} else {
(0..task.n_ops)
.map(|oi| task.del.slice(oi).iter().any(|f| protected.contains(f)))
.collect()
};
let plan_i = match solve_from(
&task,
&kind,
&state,
&groups[i].pos,
&groups[i].num,
&forbidden,
&[], threads,
tier,
)
.or_else(|| {
if forbidden.is_empty() {
None
} else {
solve_from(
&task,
&kind,
&state,
&groups[i].pos,
&groups[i].num,
&[],
&[],
threads,
tier,
)
}
}) {
Some(p) => p,
None => {
if dbg {
eprintln!("[TDECOMP] contract {i} UNSOLVABLE from current state");
}
conflict = Some((i, None));
break;
}
};
let ns = match treplay(&task, &state, &plan_i) {
Some(s) => s,
None => {
conflict = Some((i, None));
break;
}
};
if let Some(j) = (0..i)
.find(|&j| done[j] && !task.goal_met_with(&ns, &groups[j].pos, &groups[j].num))
{
conflict = Some((i, Some(j)));
break;
}
for st in &plan_i.steps {
composed.push(TimedStep {
time: st.time + offset,
action: st.action.clone(),
duration: st.duration,
});
}
record.push(ContractRec {
goal: render_subgoal(&task, &groups[i]),
offset,
plan: plan_i.clone(),
});
offset += plan_i.makespan + EPS;
state = ns;
done[i] = true;
}
if conflict.is_none() && task.goal_met_with(&state, &task.goal_pos, &task.goal_num) {
let plan = TimedPlan {
steps: composed,
makespan: offset,
};
if validate(domain, problem, &plan).is_ok() {
return Some(Decomp {
contracts: record,
plan,
monolithic: false,
});
}
if !monolithic_fallback {
return None;
}
return temporal::solve(domain, problem, threads)
.map(|p| monolithic_decomp(whole_goal.clone(), p));
}
let last = groups.len() - 1;
if dbg {
eprintln!(
"[TDECOMP] conflict={:?} -> merge (groups {} -> {})",
conflict,
groups.len(),
groups.len() - 1
);
}
match conflict {
Some((i, Some(j))) => merge_at(&mut groups, i, j),
Some((i, None)) => merge_with_neighbor(&mut groups, i),
None => merge_with_neighbor(&mut groups, last),
};
}
}
fn partition_temporal(task: &PackedTask, kind: &[Kind], mutex: &[Vec<u32>]) -> Vec<Subgoal> {
let mut groups = regress_predicate_preconds(task, kind);
let base = interaction_partition(task, mutex);
let have: FxHashSet<u32> = groups.iter().flat_map(|g| g.pos.iter().copied()).collect();
for g in base {
if g.pos.len() == 1 && have.contains(&g.pos[0]) {
continue;
}
groups.push(g);
}
order_contracts(task, kind, groups)
}
fn regress_predicate_preconds(task: &PackedTask, kind: &[Kind]) -> Vec<Subgoal> {
let goal: FxHashSet<u32> = task.goal_pos.iter().copied().collect();
let on_start_achiever = |f: u32, body: &mut dyn FnMut(usize)| {
for &end in task.add_by_fact.slice(f as usize) {
for &pf in task.pre_pos.slice(end as usize) {
for &s in task.add_by_fact.slice(pf as usize) {
if matches!(kind[s as usize], Kind::Start { .. }) {
body(s as usize);
}
}
}
}
};
let expensive = |f: u32| -> bool {
let mut hit = false;
on_start_achiever(f, &mut |s| {
if !task.pre_num.slice(s).is_empty()
|| task
.num_eff
.slice(s)
.iter()
.any(|ne| matches!(ne.op, crate::types::AssignOp::Decrease))
{
hit = true;
}
});
hit
};
let mut out: Vec<u32> = Vec::new();
let mut seen: FxHashSet<u32> = FxHashSet::default();
for &gf in &task.goal_pos {
for &oi in task.add_by_fact.slice(gf as usize) {
for &f in task.pre_pos.slice(oi as usize) {
for &start in task.add_by_fact.slice(f as usize) {
if !matches!(kind[start as usize], Kind::Start { .. }) {
continue;
}
for &pf in task.pre_pos.slice(start as usize) {
if pf != gf && !goal.contains(&pf) && expensive(pf) && seen.insert(pf) {
out.push(pf);
}
}
}
}
}
}
out.into_iter()
.map(|f| Subgoal {
pos: vec![f],
num: Vec::new(),
})
.collect()
}
fn group_resources(g: &Subgoal) -> Vec<u32> {
g.num
.iter()
.filter_map(|np| match &np.lhs {
crate::types::NExpr::Fluent(t) => Some(*t),
_ => None,
})
.collect()
}
fn order_contracts(task: &PackedTask, kind: &[Kind], groups: Vec<Subgoal>) -> Vec<Subgoal> {
let n = groups.len();
if n < 2 {
return groups;
}
let res: Vec<Vec<u32>> = groups.iter().map(group_resources).collect();
let chains: Vec<FxHashSet<u32>> = groups
.iter()
.map(|g| {
temporal::demand_resources(task, kind, &g.num)
.into_iter()
.collect()
})
.collect();
let mut after: Vec<Vec<usize>> = vec![Vec::new(); n];
let mut indeg = vec![0usize; n];
for a in 0..n {
for (b, rb) in res.iter().enumerate() {
if a != b && rb.iter().any(|r| chains[a].contains(r)) {
after[a].push(b); indeg[b] += 1;
}
}
}
let mut used = vec![false; n];
let mut order: Vec<usize> = Vec::with_capacity(n);
for _ in 0..n {
let i = (0..n)
.find(|&i| !used[i] && indeg[i] == 0)
.unwrap_or_else(|| (0..n).find(|&i| !used[i]).unwrap());
used[i] = true;
order.push(i);
for &b in &after[i] {
indeg[b] = indeg[b].saturating_sub(1);
}
}
order.into_iter().map(|i| groups[i].clone()).collect()
}