use std::collections::{HashMap, HashSet};
use crate::packed::PackedTask;
use crate::search::{plan, solve_subgoal_bounded, ClosureCost, PrefPhi, SatGuidance, SearchCfg};
use crate::types::{
Action, AssignOp, Domain, Effect, Expr, Formula, MetricDir, Problem, Sym, Term,
};
pub const COST: &str = "TOTAL-COST";
pub const COST_DISP: &str = "(TOTAL-COST)";
pub fn is_pddl3(problem: &Problem) -> bool {
problem.metric.is_some() || goal_has_pref(&problem.goal)
}
fn goal_has_pref(f: &Formula) -> bool {
match f {
Formula::Pref(_, _) => true,
Formula::And(v) | Formula::Or(v) => v.iter().any(goal_has_pref),
Formula::Not(a) => goal_has_pref(a),
Formula::Forall(_, a) | Formula::Exists(_, a) => goal_has_pref(a),
_ => false,
}
}
fn expr_has_is_violated(e: &crate::types::Expr) -> bool {
use crate::types::Expr::*;
match e {
Fluent(name, _) => name.eq_ignore_ascii_case("is-violated"),
Add(a, b) | Sub(a, b) | Mul(a, b) | Div(a, b) => {
expr_has_is_violated(a) || expr_has_is_violated(b)
}
Neg(a) => expr_has_is_violated(a),
Num(_) => false,
}
}
pub fn has_preferences(problem: &Problem) -> bool {
goal_has_pref(&problem.goal)
|| problem
.metric
.as_ref()
.is_some_and(|(_, e)| expr_has_is_violated(e))
}
pub(crate) fn unsupported_constraints(domain: &Domain, problem: &Problem) -> Option<String> {
if domain.constraints.is_empty() && problem.constraints.is_empty() {
return None;
}
Some(
"PDDL3 trajectory constraints (:constraints — always / sometime / \
at-most-once / sometime-after / sometime-before / within / hold-during / \
hold-after) are parsed but not yet enforced; ferroplan cannot honor them \
and will not silently ignore them. Remove the (:constraints ...) block, or \
model the requirement as hard goals or PDDL3 goal preferences."
.to_string(),
)
}
fn subst_term(t: &Term, b: &HashMap<Sym, Sym>) -> Term {
match t {
Term::Var(v) => b
.get(v)
.map(|o| Term::Const(o.clone()))
.unwrap_or_else(|| t.clone()),
Term::Const(_) => t.clone(),
}
}
fn subst_expr(e: &Expr, b: &HashMap<Sym, Sym>) -> Expr {
match e {
Expr::Num(n) => Expr::Num(*n),
Expr::Fluent(f, a) => Expr::Fluent(f.clone(), a.iter().map(|t| subst_term(t, b)).collect()),
Expr::Add(x, y) => Expr::Add(Box::new(subst_expr(x, b)), Box::new(subst_expr(y, b))),
Expr::Sub(x, y) => Expr::Sub(Box::new(subst_expr(x, b)), Box::new(subst_expr(y, b))),
Expr::Mul(x, y) => Expr::Mul(Box::new(subst_expr(x, b)), Box::new(subst_expr(y, b))),
Expr::Div(x, y) => Expr::Div(Box::new(subst_expr(x, b)), Box::new(subst_expr(y, b))),
Expr::Neg(x) => Expr::Neg(Box::new(subst_expr(x, b))),
}
}
pub(crate) fn subst_formula(f: &Formula, b: &HashMap<Sym, Sym>) -> Formula {
match f {
Formula::And(v) => Formula::And(v.iter().map(|x| subst_formula(x, b)).collect()),
Formula::Or(v) => Formula::Or(v.iter().map(|x| subst_formula(x, b)).collect()),
Formula::Not(a) => Formula::Not(Box::new(subst_formula(a, b))),
Formula::Atom(p, a) => {
Formula::Atom(p.clone(), a.iter().map(|t| subst_term(t, b)).collect())
}
Formula::Comp(op, l, r) => Formula::Comp(*op, subst_expr(l, b), subst_expr(r, b)),
Formula::Eq(x, y) => Formula::Eq(subst_term(x, b), subst_term(y, b)),
Formula::Pref(n, inner) => Formula::Pref(n.clone(), Box::new(subst_formula(inner, b))),
Formula::Forall(vars, inner) | Formula::Exists(vars, inner) => {
let mut b2 = b.clone();
for (v, _) in vars {
b2.remove(v);
}
let inner = Box::new(subst_formula(inner, &b2));
if matches!(f, Formula::Forall(..)) {
Formula::Forall(vars.clone(), inner)
} else {
Formula::Exists(vars.clone(), inner)
}
}
Formula::True => Formula::True,
Formula::False => Formula::False,
}
}
pub(crate) fn combos(vars: &[(Sym, Sym)], objs: &HashMap<Sym, Vec<Sym>>) -> Vec<HashMap<Sym, Sym>> {
let mut acc = vec![HashMap::new()];
for (v, ty) in vars {
let dom: &[Sym] = objs.get(ty).map(|x| x.as_slice()).unwrap_or(&[]);
let mut next = Vec::new();
for a in &acc {
for o in dom {
let mut m = a.clone();
m.insert(v.clone(), o.clone());
next.push(m);
}
}
acc = next;
}
acc
}
fn contains_pref(f: &Formula) -> bool {
match f {
Formula::Pref(_, _) => true,
Formula::And(v) | Formula::Or(v) => v.iter().any(contains_pref),
Formula::Not(a) | Formula::Forall(_, a) | Formula::Exists(_, a) => contains_pref(a),
_ => false,
}
}
fn extract_precond_prefs(f: &Formula, ctr: &mut usize) -> (Formula, Vec<(String, Formula)>) {
match f {
Formula::And(v) => {
let mut hard = Vec::new();
let mut prefs = Vec::new();
for x in v {
let (h, mut ps) = extract_precond_prefs(x, ctr);
prefs.append(&mut ps);
if !matches!(h, Formula::True) {
hard.push(h);
}
}
(Formula::And(hard), prefs)
}
Formula::Pref(name, inner) => {
let n = name.clone().unwrap_or_else(|| {
let s = format!("PCPREF{}", *ctr);
*ctr += 1;
s
});
(Formula::True, vec![(n, (**inner).clone())])
}
other => (other.clone(), Vec::new()),
}
}
fn split_goal(
g: &Formula,
hard: &mut Vec<Formula>,
prefs: &mut Vec<(String, Formula)>,
ctr: &mut usize,
objs: &HashMap<Sym, Vec<Sym>>,
) {
match g {
Formula::And(v) => v.iter().for_each(|f| split_goal(f, hard, prefs, ctr, objs)),
Formula::Forall(vars, inner) if contains_pref(inner) => {
for b in combos(vars, objs) {
split_goal(&subst_formula(inner, &b), hard, prefs, ctr, objs);
}
}
Formula::Pref(name, inner) => {
let n = name.clone().unwrap_or_else(|| {
let s = format!("PREF{}", *ctr);
*ctr += 1;
s
});
prefs.push((n, (**inner).clone()));
}
Formula::True => {}
other => hard.push(other.clone()),
}
}
#[allow(clippy::too_many_arguments)]
fn extract(
e: &Expr,
scale: f64,
w: &mut HashMap<String, f64>,
tc: &mut f64,
others: &mut HashMap<String, f64>,
konst: &mut f64,
other: &mut bool,
) {
match e {
Expr::Num(n) => *konst += scale * n,
Expr::Fluent(name, args) => {
if name == "IS-VIOLATED" {
if let Some(Term::Const(p)) = args.first() {
*w.entry(p.clone()).or_insert(0.0) += scale;
}
} else if name == COST {
*tc += scale;
} else if args.is_empty() {
*others.entry(name.clone()).or_insert(0.0) += scale;
} else {
*other = true;
}
}
Expr::Add(a, b) => {
extract(a, scale, w, tc, others, konst, other);
extract(b, scale, w, tc, others, konst, other);
}
Expr::Sub(a, b) => {
extract(a, scale, w, tc, others, konst, other);
extract(b, -scale, w, tc, others, konst, other);
}
Expr::Neg(a) => extract(a, -scale, w, tc, others, konst, other),
Expr::Mul(a, b) => match (&**a, &**b) {
(Expr::Num(c), _) => extract(b, scale * c, w, tc, others, konst, other),
(_, Expr::Num(c)) => extract(a, scale * c, w, tc, others, konst, other),
_ => *other = true,
},
Expr::Div(_, _) => *other = true,
}
}
pub(crate) fn static_predicates(domain: &Domain) -> HashSet<String> {
fn walk(e: &Effect, out: &mut HashSet<String>) {
match e {
Effect::And(v) => v.iter().for_each(|x| walk(x, out)),
Effect::Add(name, _) | Effect::Del(name, _) => {
out.insert(name.clone());
}
Effect::When(_, e) | Effect::Forall(_, e) => walk(e, out),
Effect::Num(..) => {}
}
}
let mut modified = HashSet::new();
for a in &domain.actions {
walk(&a.effect, &mut modified);
}
for e in &domain.monitors {
walk(e, &mut modified);
}
domain
.predicates
.iter()
.map(|(n, _)| n.clone())
.filter(|n| !modified.contains(n))
.collect()
}
pub(crate) fn peval_static(
f: &Formula,
statics: &HashSet<String>,
init: &HashSet<(Sym, Vec<Sym>)>,
) -> Formula {
match f {
Formula::Atom(p, args) => {
if !statics.contains(p) {
return f.clone();
}
let consts: Option<Vec<Sym>> = args
.iter()
.map(|t| match t {
Term::Const(c) => Some(c.clone()),
Term::Var(_) => None,
})
.collect();
match consts {
Some(cs) => {
if init.contains(&(p.clone(), cs)) {
Formula::True
} else {
Formula::False
}
}
None => f.clone(), }
}
Formula::Eq(Term::Const(a), Term::Const(b)) => {
if a == b {
Formula::True
} else {
Formula::False
}
}
Formula::Not(inner) => match peval_static(inner, statics, init) {
Formula::True => Formula::False,
Formula::False => Formula::True,
other => Formula::Not(Box::new(other)),
},
Formula::And(v) => {
let mut rest = Vec::new();
for x in v {
match peval_static(x, statics, init) {
Formula::True => {}
Formula::False => return Formula::False,
other => rest.push(other),
}
}
if rest.is_empty() {
Formula::True
} else {
Formula::And(rest)
}
}
Formula::Or(v) => {
let mut rest = Vec::new();
for x in v {
match peval_static(x, statics, init) {
Formula::True => return Formula::True,
Formula::False => {}
other => rest.push(other),
}
}
if rest.is_empty() {
Formula::False
} else {
Formula::Or(rest)
}
}
Formula::Forall(vars, inner) => match peval_static(inner, statics, init) {
Formula::True => Formula::True,
other => Formula::Forall(vars.clone(), Box::new(other)),
},
Formula::Exists(vars, inner) => match peval_static(inner, statics, init) {
Formula::False => Formula::False,
other => Formula::Exists(vars.clone(), Box::new(other)),
},
_ => f.clone(),
}
}
fn modified_functions(domain: &Domain) -> HashSet<String> {
fn walk(e: &Effect, out: &mut HashSet<String>) {
match e {
Effect::And(v) => v.iter().for_each(|x| walk(x, out)),
Effect::Num(_, name, _, _) => {
out.insert(name.clone());
}
Effect::When(_, e) | Effect::Forall(_, e) => walk(e, out),
_ => {}
}
}
let mut out = HashSet::new();
for a in &domain.actions {
walk(&a.effect, &mut out);
}
out
}
fn fluent_foldable(domain: &Domain, problem: &Problem, fname: &str) -> Option<String> {
let modified = modified_functions(domain);
let static_nonneg = |g: &str| -> bool {
if modified.contains(g) {
return false;
}
problem
.init_fluents
.iter()
.filter(|((n, _), _)| n == g)
.all(|(_, v)| *v >= 0.0)
};
let mut bad: Option<String> = None;
fn walk(
e: &Effect,
fname: &str,
in_forall: bool,
static_nonneg: &dyn Fn(&str) -> bool,
bad: &mut Option<String>,
) {
match e {
Effect::And(v) => v
.iter()
.for_each(|x| walk(x, fname, in_forall, static_nonneg, bad)),
Effect::When(_, e) => walk(e, fname, in_forall, static_nonneg, bad),
Effect::Forall(_, e) => walk(e, fname, true, static_nonneg, bad),
Effect::Num(op, name, _, val) if name == fname => {
let ok = !in_forall
&& matches!(op, AssignOp::Increase)
&& match val {
Expr::Num(n) => *n >= 0.0,
Expr::Fluent(g, _) => static_nonneg(g),
_ => false,
};
if !ok && bad.is_none() {
*bad = Some(format!(
"metric fluent ({fname}) is not foldable (not monotone, or under forall)"
));
}
}
_ => {}
}
}
for a in &domain.actions {
walk(&a.effect, fname, false, &static_nonneg, &mut bad);
}
bad
}
fn scaled_expr(coeff: f64, x: &Expr) -> Expr {
if coeff == 1.0 {
x.clone()
} else {
Expr::Mul(Box::new(Expr::Num(coeff)), Box::new(x.clone()))
}
}
fn collect_cost_mirror(eff: &Effect, fname: &str, coeff: f64, out: &mut Vec<Effect>) {
match eff {
Effect::And(v) => v
.iter()
.for_each(|x| collect_cost_mirror(x, fname, coeff, out)),
Effect::When(_, e) => collect_cost_mirror(e, fname, coeff, out),
Effect::Num(AssignOp::Increase, name, _, val) if name == fname => {
out.push(Effect::Num(
AssignOp::Increase,
COST.to_string(),
vec![],
scaled_expr(coeff, val),
));
}
_ => {}
}
}
pub fn preferences(goal: &Formula, objs: &HashMap<Sym, Vec<Sym>>) -> Vec<(String, Formula)> {
let mut hard = Vec::new();
let mut prefs = Vec::new();
let mut ctr = 0;
split_goal(goal, &mut hard, &mut prefs, &mut ctr, objs);
prefs
}
pub fn pref_weights(domain: &Domain, problem: &Problem) -> HashMap<String, f64> {
let mut w = HashMap::new();
let mut tc = 0.0;
let mut others = HashMap::new();
let mut other = false;
let absent = problem.metric.is_none();
if let Some((dir, e)) = &problem.metric {
let scale = if matches!(dir, MetricDir::Maximize) {
-1.0
} else {
1.0
};
let mut konst = 0.0;
extract(
e,
scale,
&mut w,
&mut tc,
&mut others,
&mut konst,
&mut other,
);
}
let objs = crate::ground::objects_by_type(domain, problem);
let mut out = HashMap::new();
let mut names: Vec<String> = preferences(&problem.goal, &objs)
.into_iter()
.map(|(n, _)| n)
.collect();
if let Ok(exp) = crate::constraints::expand(domain, problem) {
names.extend(exp.soft.into_iter().map(|(n, _)| n));
}
for n in names {
let wn = w.get(&n).copied().unwrap_or(if absent { 1.0 } else { 0.0 });
out.insert(n, wn);
}
out
}
pub struct Compiled {
pub domain: Domain,
pub problem: Problem,
pub minimize: bool,
pub maximized: bool,
pub metric_konst: f64,
pub n_prefs: usize,
pub warn_other: bool,
pub unsupported: Option<String>,
pub synthetic: HashSet<String>,
pub forgos: Vec<(String, f64)>,
pub folded_metric: bool,
}
impl Compiled {
pub fn display_metric(&self, optimized: f64) -> f64 {
let m = optimized + self.metric_konst;
if self.maximized {
-m
} else {
m
}
}
}
fn cost_monotone(domain: &Domain, problem: &Problem) -> bool {
let modified = modified_functions(domain);
let static_nonneg = |g: &str| -> bool {
!modified.contains(g)
&& problem
.init_fluents
.iter()
.filter(|((n, _), _)| n == g)
.all(|(_, v)| *v >= 0.0)
};
fn nonneg_static(e: &Expr, static_nonneg: &dyn Fn(&str) -> bool) -> bool {
match e {
Expr::Num(n) => *n >= 0.0,
Expr::Fluent(g, _) => static_nonneg(g),
Expr::Add(a, b) | Expr::Mul(a, b) | Expr::Div(a, b) => {
nonneg_static(a, static_nonneg) && nonneg_static(b, static_nonneg)
}
Expr::Sub(..) | Expr::Neg(_) => false,
}
}
fn walk(e: &Effect, static_nonneg: &dyn Fn(&str) -> bool, ok: &mut bool) {
match e {
Effect::And(v) => v.iter().for_each(|x| walk(x, static_nonneg, ok)),
Effect::Num(op, name, _, val)
if name == COST
&& !(matches!(op, AssignOp::Increase) && nonneg_static(val, static_nonneg)) =>
{
*ok = false;
}
_ => {}
}
}
let mut ok = true;
for a in &domain.actions {
walk(&a.effect, &static_nonneg, &mut ok);
}
ok
}
pub fn compile(domain: &Domain, problem: &Problem) -> Compiled {
let objs = crate::ground::objects_by_type(domain, problem);
let mut hard = Vec::new();
let mut prefs = Vec::new();
let mut ctr = 0;
split_goal(&problem.goal, &mut hard, &mut prefs, &mut ctr, &objs);
let n_prefs_total = prefs.len();
if std::env::var("FF_PREF_NO_STATIC").is_err() {
let statics = static_predicates(domain);
let init: HashSet<(Sym, Vec<Sym>)> = problem.init_atoms.iter().cloned().collect();
prefs = prefs
.into_iter()
.filter_map(|(name, phi)| match peval_static(&phi, &statics, &init) {
Formula::True => None,
simplified => Some((name, simplified)),
})
.collect();
if std::env::var("FF_RES_DEBUG").is_ok() && prefs.len() < n_prefs_total {
eprintln!(
"[P3] static simplification: dropped {} of {} preference instance(s)",
n_prefs_total - prefs.len(),
n_prefs_total
);
}
}
let mut w = HashMap::new();
let mut tc = 0.0;
let mut others: HashMap<String, f64> = HashMap::new();
let mut konst = 0.0;
let mut other = false;
let maximized = matches!(&problem.metric, Some((MetricDir::Maximize, _)));
match &problem.metric {
Some((MetricDir::Minimize, e)) => {
extract(e, 1.0, &mut w, &mut tc, &mut others, &mut konst, &mut other);
}
Some((MetricDir::Maximize, e)) => {
extract(
e,
-1.0,
&mut w,
&mut tc,
&mut others,
&mut konst,
&mut other,
);
}
None => {}
}
let metric_absent = problem.metric.is_none();
let _ = tc;
let mut d = domain.clone();
let mut p = problem.clone();
if !d.functions.iter().any(|(n, _)| n == COST) {
d.functions.push((COST.to_string(), vec![]));
}
if !p.init_fluents.iter().any(|((n, _), _)| n == COST) {
p.init_fluents.push(((COST.to_string(), vec![]), 0.0));
}
let mut metric_other = other;
let mut folded_metric = false;
for (fname, &coeff) in &others {
if coeff == 0.0 {
continue;
}
if coeff < 0.0 || fluent_foldable(domain, problem, fname).is_some() {
metric_other = true; continue;
}
for a in &mut d.actions {
let mut mirror = Vec::new();
collect_cost_mirror(&a.effect, fname, coeff, &mut mirror);
if !mirror.is_empty() {
folded_metric = true;
let mut v = vec![a.effect.clone()];
v.append(&mut mirror);
a.effect = Effect::And(v);
}
}
}
let mut pp_negative = false;
let mut pp_overflow = false;
let mut new_actions = Vec::new();
let mut pctr = 0usize;
for a in &d.actions {
let (hard_pre, pprefs) = extract_precond_prefs(&a.precond, &mut pctr);
if pprefs.is_empty() {
new_actions.push(a.clone());
continue;
}
let k = pprefs.len();
if k > 6 {
pp_overflow = true;
new_actions.push(Action {
precond: hard_pre,
..a.clone()
});
continue;
}
for mask in 0u32..(1u32 << k) {
let mut conj = vec![hard_pre.clone()];
let mut cost = 0.0;
for (i, (name, phi)) in pprefs.iter().enumerate() {
if mask & (1 << i) != 0 {
conj.push(Formula::Not(Box::new(phi.clone()))); let raw = w
.get(name)
.copied()
.unwrap_or(if metric_absent { 1.0 } else { 0.0 });
if raw < 0.0 {
pp_negative = true;
}
cost += raw.max(0.0);
} else {
conj.push(phi.clone()); }
}
let mut eff = vec![a.effect.clone()];
if cost != 0.0 {
eff.push(Effect::Num(
AssignOp::Increase,
COST.to_string(),
vec![],
Expr::Num(cost),
));
}
new_actions.push(Action {
name: a.name.clone(),
params: a.params.clone(),
precond: Formula::And(conj),
effect: Effect::And(eff),
monitored: a.monitored,
});
}
}
d.actions = new_actions;
const PLANNING: &str = "P3PLANNING";
const ENDED: &str = "P3ENDED";
d.predicates.push((PLANNING.to_string(), vec![]));
d.predicates.push((ENDED.to_string(), vec![]));
p.init_atoms.push((PLANNING.to_string(), vec![]));
for a in &mut d.actions {
a.precond = Formula::And(vec![
Formula::Atom(PLANNING.to_string(), vec![]),
a.precond.clone(),
]);
}
let mut synthetic = HashSet::new();
synthetic.insert("P3END".to_string());
d.actions.push(Action {
name: "P3END".to_string(),
params: vec![],
monitored: false,
precond: Formula::Atom(PLANNING.to_string(), vec![]),
effect: Effect::And(vec![
Effect::Del(PLANNING.to_string(), vec![]),
Effect::Add(ENDED.to_string(), vec![]),
]),
});
let mut goal_parts = hard;
let mut any_negative = false;
let mut forgos: Vec<(String, f64)> = Vec::new();
for (i, (name, phi)) in prefs.iter().enumerate() {
let col = format!("P3COLLECTED-{}", i);
d.predicates.push((col.clone(), vec![]));
let collect = format!("P3COLLECT-{}", i);
let forgo = format!("P3FORGO-{}", i);
synthetic.insert(collect.clone());
synthetic.insert(forgo.clone());
d.actions.push(Action {
name: collect,
params: vec![],
precond: Formula::And(vec![Formula::Atom(ENDED.to_string(), vec![]), phi.clone()]),
effect: Effect::Add(col.clone(), vec![]),
monitored: false,
});
let raw = w
.get(name)
.copied()
.unwrap_or(if metric_absent { 1.0 } else { 0.0 });
if raw < 0.0 {
any_negative = true;
}
forgos.push((format!("P3FORGO-{}", i), raw.max(0.0)));
d.actions.push(Action {
name: forgo,
params: vec![],
monitored: false,
precond: Formula::Atom(ENDED.to_string(), vec![]),
effect: Effect::And(vec![
Effect::Add(col.clone(), vec![]),
Effect::Num(
AssignOp::Increase,
COST.to_string(),
vec![],
Expr::Num(raw.max(0.0)),
),
]),
});
goal_parts.push(Formula::Atom(col, vec![]));
}
goal_parts.push(Formula::Atom(ENDED.to_string(), vec![]));
p.goal = Formula::And(goal_parts);
let unsupported = if any_negative || pp_negative {
Some("negative preference weight (cannot be encoded monotonically)".into())
} else if pp_overflow {
Some("an action has too many precondition preferences (>6)".into())
} else if !(tc == 0.0 || tc == 1.0) {
Some(format!(
"scaled total-cost coefficient ({}) is not supported",
tc
))
} else if !cost_monotone(domain, problem) {
Some("non-monotone total-cost (decrease/scale effects) breaks branch-and-bound".into())
} else {
None
};
Compiled {
domain: d,
problem: p,
minimize: !maximized,
maximized,
metric_konst: konst,
n_prefs: n_prefs_total,
warn_other: metric_other,
unsupported,
synthetic,
forgos,
folded_metric,
}
}
pub(crate) fn plan_cost(task: &PackedTask, ops: &[usize], cf: usize) -> f64 {
let mut s = task.initial();
for &oi in ops {
s = task.apply(oi, &s);
}
if s.fdef[cf] {
s.fv[cf]
} else {
0.0
}
}
pub struct MetricResult {
pub ops: Vec<usize>,
pub cost: f64,
pub iterations: usize,
pub proven: bool,
}
const RES_WEIGHT_DEFAULT: i64 = 0;
const COST_WEIGHT_FOLDED_DEFAULT: f64 = 0.0;
pub fn metric_optimize(
task: &PackedTask,
cost_fluent: usize,
forgos: &[(usize, f64)],
groups: &[Vec<u32>],
folded_metric: bool,
threads: usize,
) -> Option<MetricResult> {
const MAX_ITERS: usize = 10_000;
let init = task.initial();
let mut sat = build_sat_guidance(task, forgos);
sat.res = crate::resource::detect_resources(task, groups, &task.init_bits);
if !sat.res.is_empty() {
sat.res_weight = std::env::var("FF_RES_WEIGHT")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(RES_WEIGHT_DEFAULT);
sat.res_thresh = std::env::var("FF_RES_THRESH")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
if std::env::var("FF_RES_DEBUG").is_ok() {
let caps: Vec<usize> = sat.res.iter().map(|r| r.members.len() - 1).collect();
eprintln!(
"[C3] {} renewable resource(s), capacities {:?}; w={} thresh={}",
sat.res.len(),
caps,
sat.res_weight,
sat.res_thresh
);
}
}
sat.deadline = build_deadline_guidance(task, forgos);
let refine_cfg = SearchCfg::from_weights(1.0, 5.0, Some(300_000));
let cost_w = std::env::var("FF_PREF_COST_WEIGHT")
.ok()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(if folded_metric {
COST_WEIGHT_FOLDED_DEFAULT
} else {
0.0
});
if crate::features::espc() && !sat.deadline.is_empty() {
let mut best: Option<(Vec<usize>, f64)> = None;
let first = plan(
task,
threads,
SearchCfg::from_weights(1.0, 5.0, Some(1_500_000)),
true,
);
if let Some(ops) = first.ops {
let cost = plan_cost(task, &ops, cost_fluent);
if cost <= 0.0 {
return Some(MetricResult {
ops,
cost,
iterations: 0,
proven: true,
});
}
best = Some((ops, cost));
}
let part = build_espc_partition(task, forgos, groups, &sat);
return crate::espc::espc_optimize(
task,
cost_fluent,
&mut sat,
best.clone(),
part,
threads,
refine_cfg,
)
.map(|r| MetricResult {
ops: r.ops,
cost: r.cost,
iterations: r.iterations,
proven: false, })
.or_else(|| {
best.map(|(ops, cost)| MetricResult {
ops,
cost,
iterations: 0,
proven: false,
})
});
}
if !sat.deadline.is_empty() {
sat.deadline_weight = std::env::var("FF_DEADLINE_WEIGHT")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
if std::env::var("FF_RES_DEBUG").is_ok() {
eprintln!(
"[ESPC] {} deadline pair(s), fixed lambda={}",
sat.deadline.len(),
sat.deadline_weight
);
}
}
let numlegacy = folded_metric && std::env::var("FF_PREF_NUMLEGACY").is_ok();
if !forgos.is_empty() && !numlegacy && std::env::var("FF_PREF_COMPILED").is_err() {
if let Some(tail) = build_phase_tail(task, forgos) {
if let Some(r) = metric_optimize_closure(
task,
cost_fluent,
forgos,
&tail,
&sat,
groups,
threads,
refine_cfg.with_cost_weight(cost_w),
) {
return Some(r);
}
}
}
let mut bound = f64::INFINITY;
let mut best: Option<(Vec<usize>, f64)> = None;
let mut iterations = 0;
let mut proven = false;
let first = plan(
task,
threads,
SearchCfg::from_weights(1.0, 5.0, Some(1_500_000)),
true,
);
if let Some(ops) = first.ops {
let cost = plan_cost(task, &ops, cost_fluent);
if cost <= 0.0 {
return Some(MetricResult {
ops,
cost,
iterations: 0,
proven: true,
});
}
bound = cost;
best = Some((ops, cost));
}
if !forgos.is_empty() && std::env::var("FF_PREF_SEED").is_ok() {
let mut sc = crate::heuristic::Scratch::new(task);
let collect = collect_ops(task);
let mut banned: Vec<bool> = vec![false; task.n_ops];
let mut any = false;
for (i, (_, weight)) in forgos.iter().enumerate() {
let Some(ops_i) = collect.get(&i) else {
continue;
};
let mut est = f64::INFINITY;
for &oi in ops_i {
let pos: Vec<u32> = task
.pre_pos
.slice(oi)
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
let c = crate::heuristic::relaxed_plan_cost(
task,
&mut sc,
&init.bits,
&init.fv,
&init.fdef,
&pos,
task.pre_num.slice(oi),
cost_fluent,
)
.unwrap_or(f64::INFINITY);
est = est.min(c);
}
if std::env::var("FF_RES_DEBUG").is_ok() {
eprintln!("[seed] pref {i}: completion est {est:.1} vs weight {weight:.1}");
}
if est > *weight {
for &oi in ops_i {
banned[oi] = true;
}
any = true;
}
}
if any {
let (seeded, _evaluated) = crate::search::solve_subgoal_guided(
task,
&init,
&task.goal_pos,
&task.goal_num,
&banned,
threads,
SearchCfg::from_weights(1.0, 5.0, Some(1_500_000)),
None,
);
if std::env::var("FF_RES_DEBUG").is_ok() {
let c = seeded.as_ref().map(|o| plan_cost(task, o, cost_fluent));
eprintln!("[seed] seeded solve: {c:?} vs EHC incumbent {bound:.1}");
}
if let Some(ops) = seeded {
let cost = plan_cost(task, &ops, cost_fluent);
if cost < bound {
if cost <= 0.0 {
return Some(MetricResult {
ops,
cost,
iterations: 0,
proven: true,
});
}
bound = cost;
best = Some((ops, cost));
}
}
}
}
let budget = pref_eval_budget();
let no_escalate = std::env::var("FF_PREF_NO_ESCALATE").is_ok();
const PROFILES: &[(f64, f64)] = &[(1.0, 2.0), (1.0, 8.0), (2.0, 3.0), (0.0, 1.0)];
let anytime = std::env::var("FF_PREF_GREEDY").is_err();
let restarts = anytime && std::env::var("FF_PREF_NO_RESTARTS").is_err();
let mut spent = 0usize;
let mut escalated = false;
let mut rung = 0usize;
while iterations < MAX_ITERS && spent < budget {
iterations += 1;
let cap = if escalated {
budget - spent
} else if rung > 0 {
(refine_cfg.max_eval / 2).min(budget - spent)
} else {
refine_cfg.max_eval.min(budget - spent)
};
let iter_cfg = if rung == 0 {
SearchCfg {
max_eval: cap,
anytime,
..refine_cfg.with_cost_weight(cost_w)
}
} else {
let (wg, wh) = PROFILES[rung - 1];
SearchCfg {
max_eval: cap,
anytime,
..SearchCfg::from_weights(wg, wh, Some(cap)).with_cost_weight(cost_w)
}
};
let (opt, evaluated, capped) = solve_subgoal_bounded(
task,
&init,
&task.goal_pos,
&task.goal_num,
cost_fluent,
bound,
threads,
iter_cfg,
Some(&sat),
);
spent += evaluated;
match opt {
Some(ops) => {
escalated = false;
rung = 0; let cost = plan_cost(task, &ops, cost_fluent);
best = Some((ops, cost));
if cost <= 0.0 {
proven = true; break;
}
bound = cost; }
None => {
if capped && !no_escalate {
if restarts && rung < PROFILES.len() && budget > spent {
rung += 1;
continue;
}
if budget.saturating_sub(spent) > cap {
escalated = true;
rung = 0;
continue;
}
}
proven = !capped;
break;
}
}
}
best.map(|(ops, cost)| MetricResult {
ops,
cost,
iterations,
proven,
})
}
fn pref_eval_budget() -> usize {
std::env::var("FF_PREF_EVAL_BUDGET")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(2_000_000)
}
fn pref_dnf(
task: &PackedTask,
forgos: &[(usize, f64)],
) -> crate::hash::FxHashMap<usize, Vec<Vec<u32>>> {
let collect = collect_ops(task);
let mut dnf: crate::hash::FxHashMap<usize, Vec<Vec<u32>>> = crate::hash::FxHashMap::default();
for (i, _) in forgos.iter().enumerate() {
let Some(ops_i) = collect.get(&i) else {
continue;
};
let djs: Vec<Vec<u32>> = ops_i
.iter()
.filter(|&&oi| task.pre_num.slice(oi).is_empty())
.map(|&oi| {
task.pre_pos
.slice(oi)
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect()
})
.collect();
if !djs.is_empty() {
dnf.insert(i, djs);
}
}
dnf
}
type SeedOutcome = (Option<(Vec<usize>, f64)>, Option<f64>, usize);
#[allow(clippy::too_many_arguments)]
fn selection_seed(
task: &PackedTask,
cost_fluent: usize,
groups: &[Vec<u32>],
forgos: &[(usize, f64)],
tail: &PhaseTail,
p3_mask: &[bool],
threads: usize,
cfg: SearchCfg,
budget: usize,
) -> SeedOutcome {
const MAX_REPAIRS: usize = 8;
let dbg = std::env::var("FF_RES_DEBUG").is_ok();
let weights: Vec<f64> = forgos.iter().map(|&(_, w)| w).collect();
let dnf = pref_dnf(task, forgos);
let init = task.initial();
let real_goals: Vec<u32> = task
.goal_pos
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
let mut banned: crate::hash::FxHashSet<u32> = crate::hash::FxHashSet::default();
let mut spent = 0usize;
let seed_slice = budget / 4;
const PROBE_CAP: usize = 5_000;
let mut probed: crate::hash::FxHashMap<u32, bool> = crate::hash::FxHashMap::default();
let mut bound_out = None;
for round in 0..=MAX_REPAIRS {
let Some(sel) = crate::selection::select(task, groups, &weights, &dnf, &banned) else {
break;
};
if sel.capped {
if dbg {
eprintln!("[sel] skip: selection DFS node-capped");
}
return (None, None, spent);
}
if round == 0 {
bound_out = Some(sel.bound); }
let mut newly_banned = false;
let chosen_facts: Vec<u32> = {
let mut v: Vec<u32> = sel
.chosen
.iter()
.flat_map(|(_, fs)| fs.iter().copied())
.filter(|&f| !crate::bitset::test(&init.bits, f as usize))
.collect();
v.sort_unstable();
v.dedup();
v
};
for &f in &chosen_facts {
if spent >= seed_slice {
break;
}
let ok = *probed.entry(f).or_insert_with(|| {
let (ops, evaluated) = crate::search::solve_subgoal_guided(
task,
&init,
&[f],
&[],
p3_mask,
threads,
SearchCfg {
max_eval: PROBE_CAP,
..cfg
},
None,
);
spent += evaluated;
ops.is_some()
});
if !ok && banned.insert(f) {
newly_banned = true;
if dbg {
eprintln!("[sel] probe bans {}", task.fact_names[f as usize]);
}
}
}
if newly_banned {
continue; }
let mut target: Vec<u32> = real_goals.clone();
target.extend(chosen_facts.iter().copied());
target.sort_unstable();
target.dedup();
if target.is_empty() || spent >= seed_slice {
break;
}
let stage_cfg = SearchCfg {
max_eval: cfg.max_eval.min(seed_slice - spent),
..cfg
};
let (ops, evaluated) = crate::search::solve_subgoal_guided(
task,
&init,
&target,
&task.goal_num,
p3_mask,
threads,
stage_cfg,
None,
);
spent += evaluated;
match ops {
Some(mut ops) => {
let mut s = init.clone();
for &oi in &ops {
s = task.apply(oi, &s);
}
let Some(tail_ops) = apply_tail(task, &mut s, tail) else {
break;
};
ops.extend(tail_ops);
if !task.goal_met_with(&s, &task.goal_pos, &task.goal_num) {
break;
}
let cost = plan_cost(task, &ops, cost_fluent);
if dbg {
eprintln!(
"[sel] round {round}: target of {} facts reached, cost {cost} \
(selection bound {:.1}), {spent} evals",
target.len(),
sel.bound
);
}
return (Some((ops, cost)), bound_out, spent);
}
None => {
if dbg {
eprintln!("[sel] round {round}: joint target failed; no seed");
}
break;
}
}
}
if dbg {
eprintln!("[sel] no reachable selection target ({spent} evals spent)");
}
(None, bound_out, spent)
}
#[allow(clippy::too_many_arguments)]
fn compose_pref_seed(
task: &PackedTask,
cost_fluent: usize,
groups: &[Vec<u32>],
forgos: &[(usize, f64)],
tail: &PhaseTail,
sat: &SatGuidance,
p3_mask: &[bool],
threads: usize,
cfg: SearchCfg,
budget: usize,
) -> Option<(Vec<usize>, f64, usize)> {
use crate::types::eval_numpre;
let init = task.initial();
let mut spent = 0usize;
struct Cand {
pos: Vec<u32>,
num: Vec<crate::types::NumPre>,
est: f64,
value: f64, mandatory: bool,
}
let mut cands: Vec<Cand> = Vec::new();
for &g in task.goal_pos.iter().filter(|&&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
}) {
cands.push(Cand {
pos: vec![g],
num: Vec::new(),
est: 0.0,
value: f64::INFINITY,
mandatory: true,
});
}
let collect = collect_ops(task);
let mut sc = crate::heuristic::Scratch::new(task);
for (i, _) in forgos.iter().enumerate() {
let Some(ops_i) = collect.get(&i) else {
continue;
};
let weight = forgos[i].1;
let mut cheapest: Option<(Vec<u32>, Vec<crate::types::NumPre>, f64)> = None;
let mut already = false;
for &oi in ops_i {
let pos: Vec<u32> = task
.pre_pos
.slice(oi)
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
let num = task.pre_num.slice(oi).to_vec();
let true_now = pos
.iter()
.all(|&f| crate::bitset::test(&init.bits, f as usize))
&& num
.iter()
.all(|np| eval_numpre(np, &init.fv, &init.fdef) == Some(true));
if true_now {
already = true; break;
}
let est = crate::heuristic::relaxed_plan_cost(
task,
&mut sc,
&init.bits,
&init.fv,
&init.fdef,
&pos,
&num,
cost_fluent,
)
.unwrap_or(f64::INFINITY);
if cheapest.as_ref().map_or(true, |(_, _, c)| est < *c) {
cheapest = Some((pos, num, est));
}
}
if already {
continue;
}
if let Some((pos, num, est)) = cheapest {
if est <= weight && !pos.is_empty() {
cands.push(Cand {
pos,
num,
est,
value: weight - est,
mandatory: false,
});
}
}
}
let dbg = std::env::var("FF_RES_DEBUG").is_ok();
if cands.len() < 2 {
if dbg {
eprintln!("[seed3] skip: {} candidate(s)", cands.len());
}
return None;
}
let mut var_of: crate::hash::FxHashMap<u32, usize> = crate::hash::FxHashMap::default();
for (gi, g) in groups.iter().enumerate() {
for &f in g {
var_of.insert(f, gi);
}
}
{
let mut claimed: crate::hash::FxHashMap<usize, (u32, usize)> =
crate::hash::FxHashMap::default();
let mut drop = vec![false; cands.len()];
for ci in 0..cands.len() {
for k in 0..cands[ci].pos.len() {
let f = cands[ci].pos[k];
let Some(&gi) = var_of.get(&f) else { continue };
match claimed.entry(gi) {
std::collections::hash_map::Entry::Vacant(e) => {
e.insert((f, ci));
}
std::collections::hash_map::Entry::Occupied(mut o) => {
let (held_f, held_ci) = *o.get();
if held_f == f || drop[held_ci] {
if drop[held_ci] {
o.insert((f, ci));
}
continue; }
let (a, b) = (held_ci, ci);
let better_b = cands[b].value.partial_cmp(&cands[a].value)
== Some(std::cmp::Ordering::Greater);
if better_b {
drop[a] = true;
o.insert((f, b));
} else {
drop[b] = true;
}
}
}
}
}
let before = cands.len();
let mut keep = drop.iter().map(|d| !d);
cands.retain(|_| keep.next().unwrap());
if dbg && cands.len() != before {
eprintln!(
"[seed3] mutex pruning: {before} -> {} candidate(s)",
cands.len()
);
}
if cands.len() < 2 {
return None;
}
}
let var = |f: u32| var_of.get(&f).copied().unwrap_or(groups.len() + f as usize);
let mut uf: Vec<usize> = (0..cands.len()).collect();
fn find(uf: &mut [usize], mut x: usize) -> usize {
while uf[x] != x {
uf[x] = uf[uf[x]];
x = uf[x];
}
x
}
let mut owner: crate::hash::FxHashMap<usize, usize> = crate::hash::FxHashMap::default();
for (ci, cand) in cands.iter().enumerate() {
for &f in &cand.pos {
let v = var(f);
match owner.entry(v) {
std::collections::hash_map::Entry::Occupied(o) => {
let a = find(&mut uf, ci);
let b = find(&mut uf, *o.get());
uf[a.max(b)] = a.min(b);
}
std::collections::hash_map::Entry::Vacant(e) => {
e.insert(ci);
}
}
}
}
let mut comp_of: crate::hash::FxHashMap<usize, Vec<usize>> = crate::hash::FxHashMap::default();
for ci in 0..cands.len() {
let r = find(&mut uf, ci);
comp_of.entry(r).or_default().push(ci);
}
if comp_of.len() < 2 {
if dbg {
eprintln!(
"[seed3] skip: {} candidate(s) collapse into {} component(s)",
cands.len(),
comp_of.len()
);
}
return None;
}
let mut comps: Vec<Vec<usize>> = comp_of.into_values().collect();
comps.sort_by_key(|members| {
members
.iter()
.flat_map(|&ci| cands[ci].pos.iter().copied())
.min()
.unwrap_or(u32::MAX)
});
let stage_cap = (cfg.max_eval / 10).max(1_000);
let seed_budget = budget / 4;
let mut state = init.clone();
let mut plan: Vec<usize> = Vec::new();
let mut protected: crate::hash::FxHashSet<u32> = crate::hash::FxHashSet::default();
for members in &comps {
let mut alive: Vec<usize> = members.clone();
loop {
let satisfied = |ci: usize| {
cands[ci]
.pos
.iter()
.all(|&f| crate::bitset::test(&state.bits, f as usize))
&& cands[ci]
.num
.iter()
.all(|np| eval_numpre(np, &state.fv, &state.fdef) == Some(true))
};
alive.retain(|&ci| !satisfied(ci));
if alive.is_empty() {
break;
}
let mut goal: Vec<u32> = alive
.iter()
.flat_map(|&ci| cands[ci].pos.iter().copied())
.collect();
goal.sort_unstable();
goal.dedup();
let nums: Vec<crate::types::NumPre> = alive
.iter()
.flat_map(|&ci| cands[ci].num.iter().cloned())
.collect();
let forbidden: Vec<bool> = (0..task.n_ops)
.map(|oi| {
p3_mask.get(oi).copied().unwrap_or(false)
|| task.del.slice(oi).iter().any(|f| protected.contains(f))
})
.collect();
if spent >= seed_budget {
if dbg {
eprintln!("[seed3] abort: seed budget exhausted mid-composition");
}
return None;
}
let stage_cfg = SearchCfg {
max_eval: stage_cap.min(seed_budget - spent),
..cfg
};
let (ops, evaluated) = crate::search::solve_subgoal_guided(
task,
&state,
&goal,
&nums,
&forbidden,
threads,
stage_cfg,
Some(sat),
);
spent += evaluated;
match ops {
Some(ops) => {
for &oi in &ops {
state = task.apply(oi, &state);
}
plan.extend(ops);
break;
}
None => {
let worst = alive
.iter()
.copied()
.filter(|&ci| !cands[ci].mandatory)
.max_by(|&a, &b| {
cands[a]
.est
.partial_cmp(&cands[b].est)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.cmp(&b))
});
match worst {
Some(w) => alive.retain(|&ci| ci != w),
None => {
if dbg {
eprintln!("[seed3] abort: mandatory facts infeasible in a stage");
}
return None;
}
}
}
}
}
for &ci in members {
if cands[ci].mandatory {
protected.extend(cands[ci].pos.iter().copied());
}
}
}
let Some(tail_ops) = apply_tail(task, &mut state, tail) else {
if dbg {
eprintln!("[seed3] abort: phase tail failed on the composed state");
}
return None;
};
plan.extend(tail_ops);
if !task.goal_met_with(&state, &task.goal_pos, &task.goal_num) {
if dbg {
eprintln!("[seed3] abort: composed state fails the full goal");
}
return None; }
let cost = plan_cost(task, &plan, cost_fluent);
Some((plan, cost, spent))
}
#[allow(clippy::too_many_arguments)]
fn metric_optimize_closure(
task: &PackedTask,
cost_fluent: usize,
forgos: &[(usize, f64)],
tail: &PhaseTail,
sat: &SatGuidance,
groups: &[Vec<u32>],
threads: usize,
cfg: SearchCfg,
) -> Option<MetricResult> {
const MAX_ITERS: usize = 10_000;
let real_pos: Vec<u32> = task
.goal_pos
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
let closure = build_closure_cost(task, forgos);
let forbidden: Vec<bool> = (0..task.n_ops)
.map(|oi| {
let n = task.op_display[oi].to_ascii_uppercase();
n == "P3END" || n.starts_with("P3COLLECT-") || n.starts_with("P3FORGO-")
})
.collect();
let init = task.initial();
let mut best: Option<(Vec<usize>, f64)> = None;
if task.goal_met_with(&init, &real_pos, &task.goal_num) {
let mut s = init.clone();
if let Some(tail_ops) = apply_tail(task, &mut s, tail) {
if task.goal_met_with(&s, &task.goal_pos, &task.goal_num) {
let cost = plan_cost(task, &tail_ops, cost_fluent);
if cost <= 0.0 {
return Some(MetricResult {
ops: tail_ops,
cost,
iterations: 0,
proven: true,
});
}
best = Some((tail_ops, cost));
}
}
}
let budget = pref_eval_budget();
let mut spent = 0usize;
let mut iterations = 0usize;
let mut proven = false;
let mut sel_bound: Option<f64> = None;
if std::env::var("FF_PREF_NO_SELECT").is_err() && task.goal_num.is_empty() {
let (seeded, bound, _evals) = selection_seed(
task,
cost_fluent,
groups,
forgos,
tail,
&forbidden,
threads,
cfg,
budget,
);
sel_bound = bound;
if let Some((ops, cost)) = seeded {
if best.as_ref().map_or(true, |(_, c)| cost < *c) {
if cost <= 0.0 {
return Some(MetricResult {
ops,
cost,
iterations: 0,
proven: true,
});
}
best = Some((ops, cost));
}
}
}
if std::env::var("FF_PREF_SEED3").is_ok() && task.goal_num.is_empty() {
let dbg = std::env::var("FF_RES_DEBUG").is_ok();
match compose_pref_seed(
task,
cost_fluent,
groups,
forgos,
tail,
sat,
&forbidden,
threads,
cfg,
budget,
) {
Some((ops, cost, evals)) => {
spent += evals;
if best.as_ref().map_or(true, |(_, c)| cost < *c) {
if dbg {
eprintln!(
"[seed3] composed incumbent {cost} (was {:?}), {evals} evals",
best.as_ref().map(|(_, c)| *c)
);
}
if cost <= 0.0 {
return Some(MetricResult {
ops,
cost,
iterations: 0,
proven: true,
});
}
best = Some((ops, cost));
} else if dbg {
eprintln!(
"[seed3] composed {cost} LOST to incumbent {:?} ({evals} evals)",
best.as_ref().map(|(_, c)| *c)
);
}
}
None => {
if dbg {
eprintln!("[seed3] no composition");
}
}
}
}
let mut bound = best.as_ref().map_or(f64::INFINITY, |(_, c)| *c);
const PROFILES: &[(f64, f64)] = &[(1.0, 2.0), (1.0, 8.0), (2.0, 3.0), (0.0, 1.0)];
let no_escalate = std::env::var("FF_PREF_NO_ESCALATE").is_ok();
let anytime = std::env::var("FF_PREF_GREEDY").is_err();
let restarts = anytime && std::env::var("FF_PREF_NO_RESTARTS").is_err();
let mut escalated = false;
let mut rung = 0usize; while iterations < MAX_ITERS && spent < budget {
iterations += 1;
let cap = if escalated {
budget - spent
} else if rung > 0 {
(cfg.max_eval / 2).min(budget - spent)
} else {
cfg.max_eval.min(budget - spent)
};
let iter_cfg = if rung == 0 {
SearchCfg {
max_eval: cap,
anytime,
..cfg
}
} else {
let (wg, wh) = PROFILES[rung - 1];
SearchCfg {
max_eval: cap,
anytime,
w_c: cfg.w_c,
..SearchCfg::from_weights(wg, wh, Some(cap))
}
};
let (opt, evaluated, capped) = crate::search::solve_closure_bounded(
task,
&real_pos,
&task.goal_num,
cost_fluent,
bound,
&closure,
&forbidden,
threads,
iter_cfg,
Some(sat),
);
spent += evaluated;
match opt {
Some(mut ops) => {
escalated = false;
rung = 0; let mut s = init.clone();
for &oi in &ops {
s = task.apply(oi, &s);
}
let Some(tail_ops) = apply_tail(task, &mut s, tail) else {
break; };
if !task.goal_met_with(&s, &task.goal_pos, &task.goal_num) {
break; }
ops.extend(tail_ops);
let cost = plan_cost(task, &ops, cost_fluent);
best = Some((ops, cost));
if cost <= 0.0 {
proven = true;
break;
}
bound = cost;
}
None => {
if capped && !no_escalate {
if restarts && rung < PROFILES.len() && budget > spent {
rung += 1;
continue;
}
if budget.saturating_sub(spent) > cap {
escalated = true; rung = 0;
continue;
}
}
proven = !capped;
break;
}
}
}
best.map(|(ops, cost)| MetricResult {
ops,
cost,
proven: proven || sel_bound.is_some_and(|b| (cost - b).abs() < 1e-6),
iterations,
})
}
fn collect_ops(task: &PackedTask) -> std::collections::HashMap<usize, Vec<usize>> {
let mut collect_op: std::collections::HashMap<usize, Vec<usize>> =
std::collections::HashMap::new();
for oi in 0..task.n_ops {
if let Some(rest) = task.op_display[oi]
.to_ascii_uppercase()
.strip_prefix("P3COLLECT-")
{
if let Ok(i) = rest.trim().parse::<usize>() {
collect_op.entry(i).or_default().push(oi);
}
}
}
collect_op
}
pub struct PhaseTail {
pub end_op: usize,
pub prefs: Vec<(Vec<usize>, usize)>,
}
pub(crate) fn build_phase_tail(task: &PackedTask, forgos: &[(usize, f64)]) -> Option<PhaseTail> {
let end_op = (0..task.n_ops).find(|&oi| task.op_display[oi].eq_ignore_ascii_case("P3END"))?;
let mut collect = collect_ops(task);
let mut prefs = Vec::with_capacity(forgos.len());
for (i, &(forgo_op, _)) in forgos.iter().enumerate() {
prefs.push((collect.remove(&i).unwrap_or_default(), forgo_op));
}
Some(PhaseTail { end_op, prefs })
}
pub(crate) fn build_closure_cost(task: &PackedTask, forgos: &[(usize, f64)]) -> ClosureCost {
let mut collect = collect_ops(task);
let mut prefs = Vec::new();
for (i, &(_, weight)) in forgos.iter().enumerate() {
if weight <= 0.0 {
continue;
}
let disjuncts: Vec<(Vec<u32>, Vec<crate::types::NumPre>)> = collect
.remove(&i)
.unwrap_or_default()
.into_iter()
.map(|oi| {
let pos: Vec<u32> = task
.pre_pos
.slice(oi)
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
(pos, task.pre_num.slice(oi).to_vec())
})
.collect();
prefs.push((weight, PrefPhi { disjuncts }));
}
ClosureCost { prefs }
}
pub(crate) fn apply_tail(
task: &PackedTask,
state: &mut crate::packed::State,
tail: &PhaseTail,
) -> Option<Vec<usize>> {
let mut ops = Vec::with_capacity(1 + tail.prefs.len());
if !task.op_applicable(tail.end_op, state) {
return None;
}
*state = task.apply(tail.end_op, state);
ops.push(tail.end_op);
for (collects, forgo) in &tail.prefs {
let oi = collects
.iter()
.copied()
.find(|&c| task.op_applicable(c, state))
.unwrap_or(*forgo);
if !task.op_applicable(oi, state) {
return None;
}
*state = task.apply(oi, state);
ops.push(oi);
}
Some(ops)
}
fn build_espc_partition(
task: &PackedTask,
forgos: &[(usize, f64)],
groups: &[Vec<u32>],
sat: &SatGuidance,
) -> Option<crate::espc::EspcPartition> {
if !task.goal_num.is_empty() {
return None; }
let tail = build_phase_tail(task, forgos)?;
let real_goals: Vec<u32> = task
.goal_pos
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
if real_goals.is_empty() {
return None;
}
let res_member: crate::hash::FxHashSet<u32> = sat
.res
.iter()
.flat_map(|r| r.members.iter().map(|&(f, _)| f))
.collect();
let excluded: crate::hash::FxHashSet<usize> = groups
.iter()
.enumerate()
.filter(|(_, g)| g.iter().any(|f| res_member.contains(f)))
.map(|(gi, _)| gi)
.collect();
let mut comps =
crate::partition::interaction_partition_of(task, groups, &real_goals, &excluded);
comps.sort_by_key(|c| c.pos.iter().min().copied().unwrap_or(u32::MAX));
if comps.len() < 2 {
return None;
}
let deliverables: crate::hash::FxHashSet<u32> =
sat.deadline.iter().map(|&(_, d, _)| d).collect();
let mut by_cond: crate::hash::FxHashMap<u32, Vec<u32>> = crate::hash::FxHashMap::default();
for oi in 0..task.n_ops {
for ce in task.cond_effs(oi) {
for &d in &ce.add {
if !deliverables.contains(&d) {
continue;
}
for &c in &ce.cond_pos {
by_cond.entry(c).or_default().push(d);
}
}
}
}
let mut assoc: crate::hash::FxHashMap<u32, Vec<u32>> = crate::hash::FxHashMap::default();
for &g in &real_goals {
let mut ds: Vec<u32> = task
.add_by_fact
.slice(g as usize)
.iter()
.flat_map(|&oi| task.pre_pos.slice(oi as usize))
.filter_map(|p| by_cond.get(p))
.flatten()
.copied()
.collect();
if !ds.is_empty() {
ds.sort_unstable();
ds.dedup();
assoc.insert(g, ds);
}
}
if std::env::var("FF_RES_DEBUG").is_ok() {
eprintln!(
"[ESPC] partition: {} component(s) over {} real goal(s), {} excluded var(s), {} enriched goal(s)",
comps.len(),
real_goals.len(),
excluded.len(),
assoc.len()
);
}
Some(crate::espc::EspcPartition { comps, tail, assoc })
}
fn build_sat_guidance(task: &PackedTask, forgos: &[(usize, f64)]) -> SatGuidance {
let mut collect_op = collect_ops(task);
let init = task.initial();
let keep_barrier = std::env::var("FF_PREF_NO_BARRIER").is_err();
let mut prefs = Vec::new();
for (i, (_, weight)) in forgos.iter().enumerate() {
let disjuncts: Vec<(Vec<u32>, Vec<crate::types::NumPre>)> = collect_op
.remove(&i)
.unwrap_or_default()
.into_iter()
.map(|oi| {
let pos: Vec<u32> = task
.pre_pos
.slice(oi)
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
(pos, task.pre_num.slice(oi).to_vec())
})
.collect();
if disjuncts.is_empty() || disjuncts.iter().any(|(p, n)| p.is_empty() && n.is_empty()) {
continue; }
let phi = PrefPhi { disjuncts };
if !keep_barrier && phi.holds(&init) {
continue; }
prefs.push((phi, (weight * 100.0).round().max(1.0) as i64));
}
SatGuidance {
prefs,
res: Vec::new(),
res_weight: 0,
res_thresh: 0,
deadline: Vec::new(),
deadline_weight: 0,
}
}
fn build_deadline_guidance(task: &PackedTask, forgos: &[(usize, f64)]) -> Vec<(u32, u32, i64)> {
use std::collections::{HashMap, HashSet};
let collect_op = collect_ops(task);
let mut value: HashMap<u32, i64> = HashMap::new();
for (i, (_, weight)) in forgos.iter().enumerate() {
let Some(ops) = collect_op.get(&i) else {
continue;
};
let w = (*weight).round().max(1.0) as i64;
let mut facts: Vec<u32> = ops
.iter()
.flat_map(|&oi| task.pre_pos.slice(oi))
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
facts.sort_unstable();
facts.dedup();
for f in facts {
*value.entry(f).or_insert(0) += w;
}
}
if value.is_empty() {
return Vec::new();
}
let traj_pairs = std::env::var("FF_ESPC_TRAJ_PAIRS").is_ok();
let mut out: Vec<(u32, u32, i64)> = Vec::new();
let mut seen: HashSet<(u32, u32)> = HashSet::new();
for oi in 0..task.n_ops {
let uncond = task.add.slice(oi);
if uncond.len() != 1 {
continue;
}
let trigger = uncond[0];
let include_shared = traj_pairs && task.monitored[oi];
let shared_iter = task.shared_cond.iter().filter(|_| include_shared);
for ce in task.cond.slice(oi).iter().chain(shared_iter) {
for &d in &ce.add {
if let Some(&val) = value.get(&d) {
if seen.insert((trigger, d)) {
out.push((trigger, d, val));
}
}
}
}
}
out.sort_unstable();
out
}
#[cfg(test)]
mod monitor_pairs {
#[test]
fn shared_monitor_adds_emit_no_deadline_pairs() {
let dom = "(define (domain sw)
(:requirements :strips :constraints)
(:predicates (on) (off) (lamp))
(:action flip-on :precondition (off) :effect (and (not (off)) (on)))
(:action flip-off :precondition (on) :effect (and (not (on)) (off)))
(:action light :precondition (on) :effect (lamp)))";
let prob = "(define (problem sw-1) (:domain sw) (:init (off)) (:goal (off))
(:constraints (preference pv (sometime (on)))))";
let d = crate::parser::parse_domain(dom).unwrap();
let p = crate::parser::parse_problem(prob).unwrap();
let (d, p) = crate::derived::compile(&d, &p).unwrap();
let (d, p) = crate::constraints::gate(&d, &p).unwrap().unwrap();
let c = super::compile(&d, &p);
let task = crate::ground::ground_task(&c.domain, &c.problem, 1).unwrap();
assert!(
!task.shared_cond.is_empty(),
"the monitor block must exist for this test to mean anything"
);
let forgos: Vec<(usize, f64)> = c
.forgos
.iter()
.filter_map(|(name, w)| {
task.op_display
.iter()
.position(|disp| disp == name)
.map(|oi| (oi, *w))
})
.collect();
let pairs = super::build_deadline_guidance(&task, &forgos);
assert!(
pairs.is_empty(),
"monitor-artifact deadline pairs must not be emitted: {pairs:?}"
);
}
}