use crate::types::{
eval_numpre, Action, AssignOp, CompOp, Domain, Duration, Effect, Expr, Formula, NExpr, NumEff,
NumPre, Problem, Sym, Term, TimeSpec,
};
#[derive(Clone, Debug)]
pub struct SnapInfo {
pub start_action: Sym,
pub end_action: Sym,
pub running_pred: Sym,
pub duration: Duration,
pub invariant: Formula,
pub params: Vec<(Sym, Sym)>,
}
pub struct TemporalCompiled {
pub domain: Domain,
pub problem: Problem,
pub snaps: Vec<SnapInfo>,
pub til_ops: Vec<(f64, Sym)>,
}
pub fn is_temporal(domain: &Domain) -> bool {
!domain.durative_actions.is_empty()
}
fn and_formulas(parts: Vec<Formula>) -> Formula {
match parts.len() {
0 => Formula::True,
1 => parts.into_iter().next().unwrap(),
_ => Formula::And(parts),
}
}
fn and_effects(mut parts: Vec<Effect>) -> Effect {
if parts.len() == 1 {
parts.pop().unwrap()
} else {
Effect::And(parts)
}
}
fn pick_conditions(da: &crate::types::DurativeAction, when: TimeSpec) -> Formula {
and_formulas(
da.conditions
.iter()
.filter(|(t, _)| *t == when)
.map(|(_, f)| f.clone())
.collect(),
)
}
fn pick_effects(da: &crate::types::DurativeAction, when: TimeSpec) -> Vec<Effect> {
da.effects
.iter()
.filter(|(t, _)| *t == when)
.map(|(_, e)| e.clone())
.collect()
}
pub fn compile(domain: &Domain, problem: &Problem) -> TemporalCompiled {
let mut d = domain.clone();
let mut snaps = Vec::new();
for da in &domain.durative_actions {
let running = format!("RUNNING-{}", da.name);
let start_name = format!("{}-START", da.name);
let end_name = format!("{}-END", da.name);
let run_args: Vec<Term> = da
.params
.iter()
.map(|(p, _)| Term::Var(p.clone()))
.collect();
let run_types: Vec<Sym> = da.params.iter().map(|(_, t)| t.clone()).collect();
d.predicates.push((running.clone(), run_types));
let invariant = pick_conditions(da, TimeSpec::All);
let start_pre = and_formulas(vec![
pick_conditions(da, TimeSpec::Start),
invariant.clone(),
]);
let mut start_eff = pick_effects(da, TimeSpec::Start);
start_eff.push(Effect::Add(running.clone(), run_args.clone()));
d.actions.push(Action {
name: start_name.clone(),
params: da.params.clone(),
precond: start_pre,
effect: and_effects(start_eff),
monitored: false,
});
let end_pre = and_formulas(vec![
pick_conditions(da, TimeSpec::End),
invariant.clone(),
Formula::Atom(running.clone(), run_args.clone()),
]);
let mut end_eff = pick_effects(da, TimeSpec::End);
end_eff.push(Effect::Del(running.clone(), run_args.clone()));
d.actions.push(Action {
name: end_name.clone(),
params: da.params.clone(),
precond: end_pre,
effect: and_effects(end_eff),
monitored: false,
});
snaps.push(SnapInfo {
start_action: start_name,
end_action: end_name,
running_pred: running,
duration: da.duration.clone(),
invariant,
params: da.params.clone(),
});
}
d.durative_actions.clear();
let mut til_ops = Vec::new();
for (k, t) in problem.til.iter().enumerate() {
let name = format!("TIL-{k}");
let args: Vec<Term> = t.args.iter().map(|a| Term::Const(a.clone())).collect();
let eff = if t.add {
Effect::Add(t.pred.clone(), args)
} else {
Effect::Del(t.pred.clone(), args)
};
d.actions.push(Action {
name: name.clone(),
params: Vec::new(),
precond: Formula::True,
effect: eff,
monitored: false,
});
til_ops.push((t.time, name));
}
TemporalCompiled {
domain: d,
problem: problem.clone(),
snaps,
til_ops,
}
}
use crate::features::DemandMode;
use crate::ground::{ground, Outcome};
use crate::hash::FxHashMap;
use crate::heuristic::{relaxed_helpful, relaxed_to, Scratch};
use crate::packed::{PackedTask, State, StateKey};
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap, HashSet};
#[derive(Clone, Debug)]
pub struct TimedStep {
pub time: f64,
pub action: String,
pub duration: Option<f64>,
}
#[derive(Clone, Debug)]
pub struct TimedPlan {
pub steps: Vec<TimedStep>,
pub makespan: f64,
}
impl TimedPlan {
pub fn to_ipc(&self) -> String {
let mut s = String::new();
for step in &self.steps {
s.push_str(&format!(
"{:.3}: ({}) [{:.3}]\n",
step.time,
step.action.to_lowercase(),
step.duration.unwrap_or(0.001),
));
}
s
}
}
#[derive(Clone, Copy)]
pub(crate) enum Kind {
Start {
dur: f64,
end_op: usize,
},
End,
Classical,
Til,
Skip,
}
struct TNode {
state: State,
time: f64,
agenda: Vec<(f64, usize)>,
father: usize,
ev: Option<(usize, f64)>,
g: u32,
helpful: Vec<u32>,
met: Vec<i32>,
}
fn tkey(task: &PackedTask, n: &TNode) -> (StateKey, Vec<(i64, usize)>) {
let ag = n
.agenda
.iter()
.map(|&(t, o)| ((t * 1000.0).round() as i64, o))
.collect();
(task.state_key(&n.state), ag)
}
fn eval_duration(snap: &SnapInfo, args: &[&str], task: &PackedTask, init: &State) -> Option<f64> {
let bind = duration_bind(snap, args);
let d = eval_expr(snap.duration.chosen()?, &bind, task, init)?;
if d.is_finite() && d > 0.0 {
Some(d)
} else {
None
}
}
fn eval_duration_bounds(
snap: &SnapInfo,
args: &[&str],
task: &PackedTask,
init: &State,
) -> (Option<f64>, Option<f64>) {
let bind = duration_bind(snap, args);
let ev = |o: &Option<Expr>| o.as_ref().and_then(|e| eval_expr(e, &bind, task, init));
(ev(&snap.duration.min), ev(&snap.duration.max))
}
fn duration_bind<'a>(snap: &'a SnapInfo, args: &[&'a str]) -> HashMap<&'a str, &'a str> {
snap.params
.iter()
.map(|(p, _)| p.as_str())
.zip(args.iter().copied())
.collect()
}
fn eval_expr(e: &Expr, bind: &HashMap<&str, &str>, task: &PackedTask, init: &State) -> Option<f64> {
match e {
Expr::Num(n) => Some(*n),
Expr::Fluent(name, terms) => {
let mut disp = String::from("(");
disp.push_str(name);
for t in terms {
disp.push(' ');
match t {
Term::Const(c) => disp.push_str(c),
Term::Var(v) => disp.push_str(bind.get(v.as_str())?),
}
}
disp.push(')');
let id = task.fluent_id(&disp)?;
init.fdef[id].then(|| init.fv[id])
}
Expr::Add(a, b) => Some(eval_expr(a, bind, task, init)? + eval_expr(b, bind, task, init)?),
Expr::Sub(a, b) => Some(eval_expr(a, bind, task, init)? - eval_expr(b, bind, task, init)?),
Expr::Mul(a, b) => Some(eval_expr(a, bind, task, init)? * eval_expr(b, bind, task, init)?),
Expr::Div(a, b) => {
let d = eval_expr(b, bind, task, init)?;
if d == 0.0 {
return None;
}
Some(eval_expr(a, bind, task, init)? / d)
}
Expr::Neg(a) => Some(-eval_expr(a, bind, task, init)?),
}
}
pub fn solve(domain: &Domain, problem: &Problem, threads: usize) -> Option<TimedPlan> {
let ambient = crate::features::demand_mode();
if let Some(plan) = solve_monolithic(domain, problem, threads, ambient) {
return Some(plan);
}
if ambient == DemandMode::Off || !crate::features::escalate() {
return None;
}
if ambient != DemandMode::Full {
if let Some(plan) = solve_monolithic(domain, problem, threads, DemandMode::Full) {
return Some(plan);
}
}
crate::tresolve::solve_after_ladder(domain, problem, threads, ambient)
}
pub(crate) fn solve_monolithic(
domain: &Domain,
problem: &Problem,
threads: usize,
tier: DemandMode,
) -> Option<TimedPlan> {
if crate::features::tconc() {
let reduced = crate::tsched::n_actors(domain, problem) >= 2;
let solo = crate::tsched::single_actor_problem(domain, problem);
if let Some(plan) = solve_inner(domain, &solo, threads, tier) {
if let Some(rp) = crate::tsched::reschedule(domain, problem, &plan) {
return Some(rp);
}
if !reduced {
return Some(plan);
}
}
}
solve_inner(domain, problem, threads, tier)
}
fn solve_inner(
domain: &Domain,
problem: &Problem,
threads: usize,
tier: DemandMode,
) -> Option<TimedPlan> {
let c = compile(domain, problem);
let task = match ground(&c.domain, &c.problem, threads) {
Outcome::Task(t) => t,
Outcome::GoalTrue => {
return Some(TimedPlan {
steps: Vec::new(),
makespan: 0.0,
})
}
_ => return None,
};
let kind = build_kind(&task, &c);
let by_display: HashMap<&str, usize> = task
.op_display
.iter()
.enumerate()
.map(|(i, d)| (d.as_str(), i))
.collect();
let til_events: Vec<(f64, usize)> = c
.til_ops
.iter()
.filter_map(|(t, name)| by_display.get(name.as_str()).map(|&oi| (*t, oi)))
.collect();
solve_from(
&task,
&kind,
&task.initial(),
&task.goal_pos,
&task.goal_num,
&[],
&til_events,
threads,
tier,
)
}
pub(crate) fn build_kind(task: &PackedTask, c: &TemporalCompiled) -> Vec<Kind> {
let init = task.initial();
let snap_by_start: HashMap<&str, &SnapInfo> = c
.snaps
.iter()
.map(|s| (s.start_action.as_str(), s))
.collect();
let end_names: HashSet<&str> = c.snaps.iter().map(|s| s.end_action.as_str()).collect();
let til_names: HashSet<&str> = c.til_ops.iter().map(|(_, n)| n.as_str()).collect();
let by_display: HashMap<&str, usize> = task
.op_display
.iter()
.enumerate()
.map(|(i, d)| (d.as_str(), i))
.collect();
(0..task.n_ops)
.map(|oi| {
let disp = &task.op_display[oi];
let head = disp.split_whitespace().next().unwrap_or("");
if let Some(snap) = snap_by_start.get(head) {
let args: Vec<&str> = disp.split_whitespace().skip(1).collect();
let end_disp = disp.replacen("-START", "-END", 1);
match (
eval_duration(snap, &args, task, &init),
by_display.get(end_disp.as_str()),
) {
(Some(dur), Some(&end_op)) => Kind::Start { dur, end_op },
_ => Kind::Skip,
}
} else if end_names.contains(head) {
Kind::End
} else if til_names.contains(head) {
Kind::Til
} else {
Kind::Classical
}
})
.collect()
}
fn relevant_op_mask(
task: &PackedTask,
goal_pos: &[u32],
goal_num: &[NumPre],
tight: bool,
) -> Vec<bool> {
let mut rel_fact: crate::hash::FxHashSet<u32> = goal_pos.iter().copied().collect();
let mut rel_res: crate::hash::FxHashSet<u32> = goal_num
.iter()
.filter_map(|np| as_threshold(np).map(|(t, _)| t))
.collect();
let best_end: Vec<Option<usize>> = if tight {
(0..task.fv0.len())
.map(|r| best_producer(task, r as u32).map(|(o, _)| o))
.collect()
} else {
Vec::new()
};
let produces = |oi: usize, t: u32| -> bool {
!tight || best_end.get(t as usize).copied().flatten() == Some(oi)
};
let mut relevant = vec![false; task.n_ops];
loop {
let mut changed = false;
#[allow(clippy::needless_range_loop)]
for oi in 0..task.n_ops {
if relevant[oi] {
continue;
}
let touches_fact = task.add.slice(oi).iter().any(|f| rel_fact.contains(f))
|| task.del.slice(oi).iter().any(|f| rel_fact.contains(f));
let inc_res = task.num_eff.slice(oi).iter().any(|ne| {
matches!(ne.op, AssignOp::Increase)
&& rel_res.contains(&ne.target)
&& produces(oi, ne.target)
});
let cond_rel = task.cond_effs(oi).any(|ce| {
ce.add.iter().any(|f| rel_fact.contains(f))
|| ce.del.iter().any(|f| rel_fact.contains(f))
|| ce.num.iter().any(|ne| {
matches!(ne.op, AssignOp::Increase) && rel_res.contains(&ne.target)
})
});
if touches_fact || inc_res || cond_rel {
relevant[oi] = true;
changed = true;
for &f in task.pre_pos.slice(oi) {
rel_fact.insert(f);
}
for np in task.pre_num.slice(oi) {
if let Some((t, _)) = as_threshold(np) {
rel_res.insert(t);
}
}
for ne in task.num_eff.slice(oi) {
if matches!(ne.op, AssignOp::Decrease) {
rel_res.insert(ne.target);
}
}
for ce in task.cond_effs(oi) {
for &f in &ce.cond_pos {
rel_fact.insert(f);
}
for ne in &ce.num {
if matches!(ne.op, AssignOp::Decrease) {
rel_res.insert(ne.target);
}
}
}
}
}
if !changed {
break;
}
}
relevant
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn solve_from(
task: &PackedTask,
kind: &[Kind],
start: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
forbidden: &[bool],
til_events: &[(f64, usize)],
threads: usize,
tier: DemandMode,
) -> Option<TimedPlan> {
if statically_unsolvable(task, start, goal_pos, goal_num) {
return None;
}
let landmarks = extract_landmarks(task, goal_num);
let demand = if tier != DemandMode::Off {
let w = std::env::var("FF_TDEMAND_W")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(3);
let mut seed: Vec<NumPre> = goal_num.to_vec();
if tier == DemandMode::Full {
seed.extend(predicate_goal_thresholds(task, kind, goal_pos));
}
let d = compute_demand(task, kind, &seed, w);
if std::env::var("FF_RES_DEBUG").is_ok() {
let pretty: Vec<(String, i32)> = d
.res
.iter()
.map(|&(f, a)| (task.fluent_names[f as usize].clone(), a))
.collect();
eprintln!("[TDEMAND] w={w} total={} resources={:?}", d.total, pretty);
}
d
} else {
Demand::empty()
};
let on = tier != DemandMode::Off && std::env::var("FF_NOREL").is_err();
let sound = if on {
relevant_op_mask(task, goal_pos, goal_num, false)
} else {
Vec::new()
};
let tight = if on {
relevant_op_mask(task, goal_pos, goal_num, true)
} else {
Vec::new()
};
if on && std::env::var("FF_RES_DEBUG").is_ok() {
eprintln!(
"[TREL] sound {}/{} tight {}/{}",
sound.iter().filter(|&&b| b).count(),
sound.len(),
tight.iter().filter(|&&b| b).count(),
tight.len()
);
}
let go = |rel: &[bool], prune: bool| {
temporal_search(
task, kind, &landmarks, &demand, start, goal_pos, goal_num, forbidden, rel, til_events,
prune, threads,
)
};
go(&sound, true)
.or_else(|| if on { go(&tight, false) } else { None })
.or_else(|| go(&sound, false))
.or_else(|| if on { go(&[], false) } else { None })
}
pub(crate) fn statically_unsolvable(
task: &PackedTask,
start: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
) -> bool {
let fact_true = |f: u32| (start.bits[f as usize / 64] >> (f as usize % 64)) & 1 == 1;
let never_raises = |ne: &NumEff| match (&ne.op, &ne.value) {
(AssignOp::Increase, NExpr::Num(w)) => w <= &0.0,
(AssignOp::Decrease, NExpr::Num(w)) => w >= &0.0,
_ => false,
};
let some_op_adds = |g: u32| {
(0..task.n_ops).any(|oi| {
task.add.slice(oi).contains(&g) || task.cond_effs(oi).any(|ce| ce.add.contains(&g))
})
};
let some_op_raises = |t: u32| {
(0..task.n_ops).any(|oi| {
task.num_eff
.slice(oi)
.iter()
.any(|ne| ne.target == t && !never_raises(ne))
|| task
.cond
.slice(oi)
.iter()
.any(|ce| ce.num.iter().any(|ne| ne.target == t && !never_raises(ne)))
})
};
for &g in goal_pos {
if !fact_true(g) && !some_op_adds(g) {
return true;
}
}
for np in goal_num {
if let Some((t, _)) = as_threshold(np) {
let already = eval_numpre(np, &start.fv, &start.fdef) == Some(true);
if !already && !some_op_raises(t) {
return true;
}
}
}
false
}
fn as_threshold(np: &NumPre) -> Option<(u32, f64)> {
match (&np.op, &np.lhs, &np.rhs) {
(CompOp::Ge | CompOp::Gt, NExpr::Fluent(t), NExpr::Num(w)) => Some((*t, *w)),
_ => None,
}
}
fn predicate_goal_thresholds(task: &PackedTask, kind: &[Kind], goal_pos: &[u32]) -> Vec<NumPre> {
let mut out: Vec<NumPre> = Vec::new();
let collect_thr = |oi: usize, out: &mut Vec<NumPre>| {
for pre in task.pre_num.slice(oi) {
if as_threshold(pre).is_some() {
out.push(pre.clone());
}
}
};
for &gf in goal_pos {
for &oi in task.add_by_fact.slice(gf as usize) {
let oi = oi as usize;
collect_thr(oi, &mut out); for &f in task.pre_pos.slice(oi) {
for &start in task.add_by_fact.slice(f as usize) {
if matches!(kind[start as usize], Kind::Start { .. }) {
collect_thr(start as usize, &mut out); }
}
}
}
}
out
}
fn extract_landmarks(task: &PackedTask, seed: &[NumPre]) -> Vec<NumPre> {
let mut out: Vec<NumPre> = Vec::new();
let mut seen: HashSet<(u32, u64)> = HashSet::new();
let mut work: Vec<NumPre> = seed.to_vec();
let mut iters = 0usize;
while let Some(np) = work.pop() {
iters += 1;
if iters > 8000 {
break; }
let Some((t, w)) = as_threshold(&np) else {
continue;
};
if !seen.insert((t, w.to_bits())) {
continue;
}
out.push(np.clone());
let add_pre_num = |oi: usize, work: &mut Vec<NumPre>| {
for pre in task.pre_num.slice(oi) {
if as_threshold(pre).is_some() {
work.push(pre.clone());
}
}
};
for &oi in task.neff_by_fluent.slice(t as usize) {
let oi = oi as usize;
let increases = task
.num_eff
.slice(oi)
.iter()
.any(|ne| ne.target == t && matches!(ne.op, AssignOp::Increase));
if !increases {
continue;
}
add_pre_num(oi, &mut work);
for &f in task.pre_pos.slice(oi) {
for &start in task.add_by_fact.slice(f as usize) {
add_pre_num(start as usize, &mut work);
}
}
}
}
out
}
fn landmark_deficit(landmarks: &[NumPre], fv: &[f64], fdef: &[bool]) -> i64 {
landmarks
.iter()
.map(|np| match as_threshold(np) {
Some((t, want)) => {
let cur = if fdef[t as usize] {
fv[t as usize]
} else {
0.0
};
(want - cur).max(0.0).ceil() as i64
}
None => 0,
})
.sum()
}
struct Demand {
res: Vec<(u32, i32)>,
idx: FxHashMap<u32, usize>,
total: i32,
weight: i64,
}
impl Demand {
fn empty() -> Self {
Demand {
res: Vec::new(),
idx: FxHashMap::default(),
total: 0,
weight: 0,
}
}
}
fn best_producer(task: &PackedTask, t: u32) -> Option<(usize, i32)> {
let mut best: Option<(usize, i32)> = None;
for &oi in task.neff_by_fluent.slice(t as usize) {
let oi = oi as usize;
for ne in task.num_eff.slice(oi) {
if ne.target == t && matches!(ne.op, AssignOp::Increase) {
let y = ne.value.eval(&task.fv0, &task.fdef0).unwrap_or(0.0);
if y > 0.0 {
let yi = y.ceil() as i32;
if best.map_or(true, |(_, by)| yi > by) {
best = Some((oi, yi));
}
}
}
}
}
best
}
fn compute_demand(task: &PackedTask, kind: &[Kind], seed: &[NumPre], weight: i64) -> Demand {
use crate::hash::FxHashSet;
const MAX_ITERS: usize = 20_000;
const CAP: i32 = 100_000; let mut need: FxHashMap<u32, i32> = FxHashMap::default();
let mut work: Vec<(u32, i32)> = seed
.iter()
.filter_map(|np| as_threshold(np).map(|(t, w)| (t, w.ceil().max(0.0) as i32)))
.collect();
let mut iters = 0usize;
while let Some((t, amt)) = work.pop() {
iters += 1;
if iters > MAX_ITERS {
break;
}
if amt <= 0 {
continue;
}
let cur = need.entry(t).or_insert(0);
let target = (*cur + amt).min(CAP);
let delta = target - *cur; if delta <= 0 {
continue;
}
*cur = target;
let Some((oi, yield_t)) = best_producer(task, t) else {
continue; };
let apps = (delta + yield_t - 1) / yield_t; let mut consumers: FxHashSet<usize> = FxHashSet::default();
consumers.insert(oi);
for &f in task.pre_pos.slice(oi) {
for &start in task.add_by_fact.slice(f as usize) {
if matches!(kind[start as usize], Kind::Start { .. }) {
consumers.insert(start as usize);
}
}
}
for op in consumers {
for ne in task.num_eff.slice(op) {
if matches!(ne.op, AssignOp::Decrease) {
let c = ne.value.eval(&task.fv0, &task.fdef0).unwrap_or(0.0);
if c > 0.0 {
work.push((ne.target, apps.saturating_mul(c.ceil() as i32)));
}
}
}
}
}
let mut res: Vec<(u32, i32)> = need.into_iter().collect();
res.sort_unstable(); let mut idx = FxHashMap::default();
let mut total = 0i32;
for (i, &(f, a)) in res.iter().enumerate() {
idx.insert(f, i);
total += a;
}
Demand {
res,
idx,
total,
weight,
}
}
pub(crate) fn demand_resources(task: &PackedTask, kind: &[Kind], goal_num: &[NumPre]) -> Vec<u32> {
compute_demand(task, kind, goal_num, 1)
.res
.into_iter()
.map(|(f, _)| f)
.collect()
}
fn met_root(demand: &Demand, task: &PackedTask) -> Vec<i32> {
demand
.res
.iter()
.map(|&(f, a)| {
let cur = if task.fdef0[f as usize] {
task.fv0[f as usize]
} else {
0.0
};
(cur.max(0.0) as i32).min(a)
})
.collect()
}
fn met_child(parent: &[i32], demand: &Demand, task: &PackedTask, oi: usize) -> Vec<i32> {
if demand.res.is_empty() {
return Vec::new();
}
let mut m = parent.to_vec();
for ne in task.num_eff.slice(oi) {
if matches!(ne.op, AssignOp::Increase) {
if let Some(&i) = demand.idx.get(&ne.target) {
let v = ne.value.eval(&task.fv0, &task.fdef0).unwrap_or(0.0);
if v > 0.0 {
m[i] = (m[i] + v.ceil() as i32).min(demand.res[i].1);
}
}
}
}
m
}
#[inline]
fn demand_deficit(met: &[i32], demand: &Demand) -> i64 {
(demand.total - met.iter().sum::<i32>()) as i64
}
#[allow(clippy::too_many_arguments)]
fn eval_node(
task: &PackedTask,
kind: &[Kind],
sc: &mut Scratch,
s: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
prune: bool,
) -> Option<(i32, Vec<u32>)> {
if prune {
let (h, helpful) = relaxed_helpful(task, sc, &s.bits, &s.fv, &s.fdef, goal_pos, goal_num)?;
let hf = helpful
.into_iter()
.filter(|&oi| matches!(kind[oi as usize], Kind::Start { .. } | Kind::Classical))
.collect();
Some((h, hf))
} else {
let h = relaxed_to(task, sc, &s.bits, &s.fv, &s.fdef, goal_pos, goal_num)?;
Some((h, Vec::new()))
}
}
#[allow(clippy::too_many_arguments)]
fn enqueue_evaluated(
task: &PackedTask,
nodes: &mut Vec<TNode>,
heap: &mut BinaryHeap<Reverse<(i64, usize)>>,
visited: &mut HashSet<(StateKey, Vec<(i64, usize)>)>,
landmarks: &[NumPre],
demand: &Demand,
prune: bool,
mut n: TNode,
h: i32,
helpful: Vec<u32>,
) {
const W_G: i64 = 1;
const W_H: i64 = 3;
const W_L: i64 = 3;
const AGENDA_W: i64 = 0;
let k = tkey(task, &n);
if visited.insert(k) {
n.helpful = helpful;
let op = n.ev.map(|(o, _)| o).unwrap_or(usize::MAX);
n.met = if op == usize::MAX {
nodes[n.father].met.clone()
} else {
met_child(&nodes[n.father].met, demand, task, op)
};
let key = if prune {
W_G * n.g as i64
+ W_H * h as i64
+ W_L * landmark_deficit(landmarks, &n.state.fv, &n.state.fdef)
+ demand.weight * demand_deficit(&n.met, demand)
+ AGENDA_W * n.agenda.len() as i64
} else {
h as i64
};
let idx = nodes.len();
nodes.push(n);
heap.push(Reverse((key, idx)));
}
}
#[allow(clippy::too_many_arguments)]
fn push_node(
task: &PackedTask,
kind: &[Kind],
sc: &mut Scratch,
nodes: &mut Vec<TNode>,
heap: &mut BinaryHeap<Reverse<(i64, usize)>>,
visited: &mut HashSet<(StateKey, Vec<(i64, usize)>)>,
landmarks: &[NumPre],
demand: &Demand,
goal_pos: &[u32],
goal_num: &[NumPre],
prune: bool,
n: TNode,
) {
if let Some((h, helpful)) = eval_node(task, kind, sc, &n.state, goal_pos, goal_num, prune) {
enqueue_evaluated(
task, nodes, heap, visited, landmarks, demand, prune, n, h, helpful,
);
}
}
#[allow(clippy::too_many_arguments)]
fn temporal_search(
task: &PackedTask,
kind: &[Kind],
landmarks: &[NumPre],
demand: &Demand,
start: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
forbidden: &[bool],
relevant: &[bool],
til_events: &[(f64, usize)],
prune: bool,
threads: usize,
) -> Option<TimedPlan> {
const MAX_NODES: usize = 400_000;
let workers = if threads == 0 {
crate::par::num_threads()
} else {
threads
};
let init = start.clone();
let mut sc = Scratch::new(task);
let (_h0, hf0) = eval_node(task, kind, &mut sc, &init, goal_pos, goal_num, prune)?; let mut root_agenda: Vec<(f64, usize)> = til_events.to_vec();
root_agenda.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut nodes = vec![TNode {
state: init,
time: 0.0,
agenda: root_agenda,
father: usize::MAX,
ev: None,
g: 0,
helpful: hf0,
met: met_root(demand, task),
}];
let mut heap: BinaryHeap<Reverse<(i64, usize)>> = BinaryHeap::new();
heap.push(Reverse((0, 0)));
let mut visited: HashSet<(StateKey, Vec<(i64, usize)>)> = HashSet::new();
visited.insert(tkey(task, &nodes[0]));
while let Some(Reverse((_k, ni))) = heap.pop() {
let ends_pending = nodes[ni]
.agenda
.iter()
.any(|&(_, op)| !matches!(kind[op], Kind::Til));
if task.goal_met_with(&nodes[ni].state, goal_pos, goal_num) && !ends_pending {
let plan = reconstruct(task, &nodes, ni, kind);
return Some(epsilon_separate(task, plan, !til_events.is_empty()));
}
if nodes.len() > MAX_NODES {
break;
}
let time = nodes[ni].time;
let pg = nodes[ni].g;
let allow = |oi: usize| {
!forbidden.get(oi).copied().unwrap_or(false)
&& (relevant.is_empty() || relevant.get(oi).copied().unwrap_or(true))
};
let candidates: Vec<usize> = if prune && !nodes[ni].helpful.is_empty() {
nodes[ni]
.helpful
.iter()
.map(|&o| o as usize)
.filter(|&oi| allow(oi))
.collect()
} else {
(0..task.n_ops).filter(|&oi| allow(oi)).collect()
};
let mut protos: Vec<TNode> = Vec::new();
for oi in candidates {
match kind[oi] {
Kind::Start { dur, end_op } => {
if task.op_applicable(oi, &nodes[ni].state) {
let ns = task.apply(oi, &nodes[ni].state);
let mut ag = nodes[ni].agenda.clone();
let te = time + dur;
let pos = ag.partition_point(|x| x.0 <= te);
ag.insert(pos, (te, end_op));
protos.push(TNode {
state: ns,
time,
agenda: ag,
father: ni,
ev: Some((oi, time)),
g: pg + 1,
helpful: Vec::new(),
met: Vec::new(),
});
}
}
Kind::Classical => {
if task.op_applicable(oi, &nodes[ni].state) {
let ns = task.apply(oi, &nodes[ni].state);
let ag = nodes[ni].agenda.clone();
protos.push(TNode {
state: ns,
time,
agenda: ag,
father: ni,
ev: Some((oi, time)),
g: pg + 1,
helpful: Vec::new(),
met: Vec::new(),
});
}
}
Kind::End | Kind::Til | Kind::Skip => {}
}
}
const PAR_FRONTIER: usize = 128;
if workers <= 1 || protos.len() < PAR_FRONTIER {
for n in protos {
push_node(
task,
kind,
&mut sc,
&mut nodes,
&mut heap,
&mut visited,
landmarks,
demand,
goal_pos,
goal_num,
prune,
n,
);
}
} else {
let evals: Vec<Option<(i32, Vec<u32>)>> = crate::par::par_map_with(
&protos,
workers,
|| Scratch::new(task),
|wsc, n| eval_node(task, kind, wsc, &n.state, goal_pos, goal_num, prune),
);
for (n, ev) in protos.into_iter().zip(evals) {
if let Some((h, helpful)) = ev {
enqueue_evaluated(
task,
&mut nodes,
&mut heap,
&mut visited,
landmarks,
demand,
prune,
n,
h,
helpful,
);
}
}
}
if let Some(&(te, end_op)) = nodes[ni].agenda.first() {
if task.op_applicable(end_op, &nodes[ni].state) {
let ns = task.apply(end_op, &nodes[ni].state);
let ag = nodes[ni].agenda[1..].to_vec();
push_node(
task,
kind,
&mut sc,
&mut nodes,
&mut heap,
&mut visited,
landmarks,
demand,
goal_pos,
goal_num,
prune,
TNode {
state: ns,
time: te,
agenda: ag,
father: ni,
ev: Some((end_op, te)),
g: pg + 1,
helpful: Vec::new(),
met: Vec::new(),
},
);
}
}
}
None
}
fn reconstruct(task: &PackedTask, nodes: &[TNode], goal: usize, kind: &[Kind]) -> TimedPlan {
let mut events: Vec<(usize, f64)> = Vec::new();
let mut cur = goal;
while let Some((op, t)) = nodes[cur].ev {
events.push((op, t));
cur = nodes[cur].father;
}
events.reverse();
let mut steps = Vec::new();
let mut makespan = 0.0f64;
for (op, t) in events {
let disp = &task.op_display[op];
let head = disp.split_whitespace().next().unwrap_or("");
let args = disp
.split_whitespace()
.skip(1)
.collect::<Vec<_>>()
.join(" ");
let (name, duration) = match kind[op] {
Kind::End => {
makespan = makespan.max(t);
continue;
}
Kind::Til => continue,
Kind::Start { dur, .. } => {
makespan = makespan.max(t + dur);
(head.trim_end_matches("-START"), Some(dur))
}
_ => {
makespan = makespan.max(t);
(head, None)
}
};
let action = if args.is_empty() {
name.to_string()
} else {
format!("{} {}", name, args)
};
steps.push(TimedStep {
time: t,
action,
duration,
});
}
TimedPlan { steps, makespan }
}
pub fn validate(domain: &Domain, problem: &Problem, plan: &TimedPlan) -> Result<(), String> {
let c = compile(domain, problem);
let task = match ground(&c.domain, &c.problem, 1) {
Outcome::Task(t) => t,
Outcome::GoalTrue => {
return if plan.steps.is_empty() {
Ok(())
} else {
Err("goal is already true but the plan is non-empty".into())
}
}
_ => return Err("problem grounds to unsolvable".into()),
};
let init = task.initial();
let snap_by_start: HashMap<&str, &SnapInfo> = c
.snaps
.iter()
.map(|s| (s.start_action.as_str(), s))
.collect();
let find = |disp: &str| {
task.op_display
.iter()
.position(|d| d == disp)
.ok_or_else(|| format!("plan references unknown action `{disp}`"))
};
struct Happening {
time: f64,
op: usize,
is_start: bool,
}
let mut happenings: Vec<Happening> = Vec::new();
for step in &plan.steps {
let mut it = step.action.splitn(2, ' ');
let head = it.next().unwrap_or("");
let rest = it.next();
let with = |suffix: &str| match rest {
Some(r) => format!("{head}{suffix} {r}"),
None => format!("{head}{suffix}"),
};
match step.duration {
Some(dur) => {
let start_name = format!("{head}-START");
let snap = snap_by_start
.get(start_name.as_str())
.ok_or_else(|| format!("`{head}` is not a durative action"))?;
let args: Vec<&str> = rest
.map(|r| r.split_whitespace().collect())
.unwrap_or_default();
let (lo, hi) = eval_duration_bounds(snap, &args, &task, &init);
if let Some(min) = lo {
if dur < min - 1e-6 {
return Err(format!(
"`{}` has duration {dur} below the domain minimum {min}",
step.action
));
}
}
if let Some(max) = hi {
if dur > max + 1e-6 {
return Err(format!(
"`{}` has duration {dur} above the domain maximum {max}",
step.action
));
}
}
happenings.push(Happening {
time: step.time,
op: find(&with("-START"))?,
is_start: true,
});
happenings.push(Happening {
time: step.time + dur,
op: find(&with("-END"))?,
is_start: false,
});
}
None => happenings.push(Happening {
time: step.time,
op: find(&step.action)?,
is_start: true,
}),
}
}
let horizon = happenings.iter().map(|h| h.time).fold(0.0f64, f64::max);
for (t, name) in &c.til_ops {
if *t <= horizon + EPS {
happenings.push(Happening {
time: *t,
op: find(name)?,
is_start: false,
});
}
}
happenings.sort_by_key(|h| ((h.time / EPS).round() as i64, h.is_start));
let mut state = init.clone();
for h in &happenings {
if !task.op_applicable(h.op, &state) {
return Err(format!(
"at t={:.3}, `{}` is not applicable (precondition or invariant violated)",
h.time, task.op_display[h.op]
));
}
state = task.apply(h.op, &state);
}
if !task.goal_met(&state) {
return Err("the plan does not achieve the goal".into());
}
Ok(())
}
pub(crate) fn treplay(task: &PackedTask, state: &State, plan: &TimedPlan) -> Option<State> {
let find = |disp: &str| task.op_display.iter().position(|d| d == disp);
struct H {
time: f64,
op: usize,
is_start: bool,
}
let mut hs: Vec<H> = Vec::new();
for step in &plan.steps {
let mut it = step.action.splitn(2, ' ');
let head = it.next().unwrap_or("");
let rest = it.next();
let with = |suffix: &str| match rest {
Some(r) => format!("{head}{suffix} {r}"),
None => format!("{head}{suffix}"),
};
match step.duration {
Some(dur) => {
hs.push(H {
time: step.time,
op: find(&with("-START"))?,
is_start: true,
});
hs.push(H {
time: step.time + dur,
op: find(&with("-END"))?,
is_start: false,
});
}
None => hs.push(H {
time: step.time,
op: find(&step.action)?,
is_start: true,
}),
}
}
hs.sort_by_key(|h| ((h.time / EPS).round() as i64, h.is_start));
let mut s = state.clone();
for h in &hs {
if !task.op_applicable(h.op, &s) {
return None;
}
s = task.apply(h.op, &s);
}
Some(s)
}
const EPS: f64 = 0.001;
fn slices_intersect(a: &[u32], b: &[u32]) -> bool {
a.iter().any(|x| b.contains(x))
}
fn ops_mutex(task: &PackedTask, o1: usize, o2: usize) -> bool {
let (p1, a1, d1) = (
task.pre_pos.slice(o1),
task.add.slice(o1),
task.del.slice(o1),
);
let (p2, a2, d2) = (
task.pre_pos.slice(o2),
task.add.slice(o2),
task.del.slice(o2),
);
slices_intersect(a1, d2)
|| slices_intersect(d1, a2)
|| slices_intersect(a1, p2)
|| slices_intersect(p1, a2)
|| slices_intersect(d1, p2)
|| slices_intersect(p1, d2)
}
fn epsilon_separate(task: &PackedTask, plan: TimedPlan, floor_to_search: bool) -> TimedPlan {
struct H {
op: usize,
step: usize,
is_start: bool,
time: f64,
}
let find = |disp: &str| task.op_display.iter().position(|d| d == disp);
let mut hs: Vec<H> = Vec::new();
for (si, step) in plan.steps.iter().enumerate() {
let mut it = step.action.splitn(2, ' ');
let head = it.next().unwrap_or("");
let rest = it.next();
match step.duration {
Some(dur) => {
let sd = match rest {
Some(r) => format!("{head}-START {r}"),
None => format!("{head}-START"),
};
let ed = match rest {
Some(r) => format!("{head}-END {r}"),
None => format!("{head}-END"),
};
match (find(&sd), find(&ed)) {
(Some(so), Some(eo)) => {
hs.push(H {
op: so,
step: si,
is_start: true,
time: step.time,
});
hs.push(H {
op: eo,
step: si,
is_start: false,
time: step.time + dur,
});
}
_ => return plan, }
}
None => match find(&step.action) {
Some(o) => hs.push(H {
op: o,
step: si,
is_start: true,
time: step.time,
}),
None => return plan,
},
}
}
let n = hs.len();
if n == 0 || n > 600 {
return plan; }
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
hs[a]
.time
.partial_cmp(&hs[b].time)
.unwrap_or(std::cmp::Ordering::Equal)
.then(hs[a].is_start.cmp(&hs[b].is_start))
});
let mut edges: Vec<(usize, usize, f64)> = Vec::new();
for w in order.windows(2) {
edges.push((w[0], w[1], 0.0));
}
for i in 0..n {
for j in (i + 1)..n {
let (u, v) = (order[i], order[j]);
if ops_mutex(task, hs[u].op, hs[v].op) {
edges.push((u, v, EPS));
}
}
}
for si in 0..plan.steps.len() {
if let Some(dur) = plan.steps[si].duration {
let (mut s, mut e) = (None, None);
for (hi, h) in hs.iter().enumerate() {
if h.step == si {
if h.is_start {
s = Some(hi)
} else {
e = Some(hi)
}
}
}
if let (Some(s), Some(e)) = (s, e) {
edges.push((s, e, dur));
edges.push((e, s, -dur));
}
}
}
let mut t: Vec<f64> = if floor_to_search {
hs.iter().map(|h| h.time).collect()
} else {
vec![0.0f64; n]
};
for _ in 0..n {
let mut changed = false;
for &(u, v, w) in &edges {
if t[v] < t[u] + w - 1e-12 {
t[v] = t[u] + w;
changed = true;
}
}
if !changed {
break;
}
}
for &(u, v, w) in &edges {
if t[v] < t[u] + w - 1e-12 {
return plan; }
}
let mut steps = plan.steps;
for (hi, h) in hs.iter().enumerate() {
if h.is_start {
steps[h.step].time = t[hi];
}
}
let makespan = steps
.iter()
.map(|s| s.time + s.duration.unwrap_or(0.0))
.fold(0.0f64, f64::max);
TimedPlan { steps, makespan }
}