use std::collections::{HashMap, HashSet};
use crate::packed::PackedTask;
use crate::search::{plan, solve_subgoal_bounded, 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))
}
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))),
}
}
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,
}
}
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()),
}
}
fn extract(
e: &Expr,
scale: f64,
w: &mut HashMap<String, f64>,
tc: &mut f64,
others: &mut HashMap<String, f64>,
other: &mut bool,
) {
match e {
Expr::Num(_) => {}
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, other);
extract(b, scale, w, tc, others, other);
}
Expr::Sub(a, b) => {
extract(a, scale, w, tc, others, other);
extract(b, -scale, w, tc, others, other);
}
Expr::Neg(a) => extract(a, -scale, w, tc, others, other),
Expr::Mul(a, b) => match (&**a, &**b) {
(Expr::Num(c), _) => extract(b, scale * c, w, tc, others, other),
(_, Expr::Num(c)) => extract(a, scale * c, w, tc, others, other),
_ => *other = true,
},
Expr::Div(_, _) => *other = true,
}
}
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((_, e)) = &problem.metric {
extract(e, 1.0, &mut w, &mut tc, &mut others, &mut other);
}
let objs = crate::ground::objects_by_type(domain, problem);
let mut out = HashMap::new();
for (n, _) in preferences(&problem.goal, &objs) {
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 n_prefs: usize,
pub warn_other: bool,
pub unsupported: Option<String>,
pub synthetic: HashSet<String>,
pub forgos: Vec<(String, f64)>,
}
fn cost_monotone(domain: &Domain) -> bool {
fn walk(e: &Effect, ok: &mut bool) {
match e {
Effect::And(v) => v.iter().for_each(|x| walk(x, ok)),
Effect::Num(op, name, _, val) if name == COST => {
let good =
matches!(op, AssignOp::Increase) && matches!(val, Expr::Num(n) if *n >= 0.0);
if !good {
*ok = false;
}
}
_ => {}
}
}
let mut ok = true;
for a in &domain.actions {
walk(&a.effect, &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 mut w = HashMap::new();
let mut tc = 0.0;
let mut others: HashMap<String, f64> = HashMap::new();
let mut other = false;
let minimize = match &problem.metric {
Some((MetricDir::Minimize, e)) => {
extract(e, 1.0, &mut w, &mut tc, &mut others, &mut other);
true
}
Some((MetricDir::Maximize, e)) => {
extract(e, 1.0, &mut w, &mut tc, &mut others, &mut other);
false
}
None => true,
};
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;
for (fname, &coeff) in &others {
if coeff == 0.0 {
continue;
}
if 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() {
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),
});
}
}
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![],
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![]),
});
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![],
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 !minimize {
Some("metric maximization is not supported (phase 1: minimize only)".into())
} else 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) {
Some("non-monotone total-cost (decrease/scale effects) breaks branch-and-bound".into())
} else {
None
};
Compiled {
domain: d,
problem: p,
minimize,
n_prefs: prefs.len(),
warn_other: metric_other,
unsupported,
synthetic,
forgos,
}
}
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;
pub fn metric_optimize(
task: &PackedTask,
cost_fluent: usize,
forgos: &[(usize, f64)],
groups: &[Vec<u32>],
threads: usize,
) -> Option<MetricResult> {
const MAX_ITERS: usize = 10_000;
let init = task.initial();
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));
}
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));
if std::env::var("FF_ESPC").is_ok() && !sat.deadline.is_empty() {
return crate::espc::espc_optimize(
task,
cost_fluent,
&mut sat,
best.clone(),
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
);
}
}
while iterations < MAX_ITERS {
iterations += 1;
let (opt, capped) = solve_subgoal_bounded(
task,
&init,
&task.goal_pos,
&task.goal_num,
cost_fluent,
bound,
threads,
refine_cfg,
Some(&sat),
);
match opt {
Some(ops) => {
let cost = plan_cost(task, &ops, cost_fluent);
best = Some((ops, cost));
if cost <= 0.0 {
proven = true; break;
}
bound = cost; }
None => {
proven = !capped;
break;
}
}
}
best.map(|(ops, cost)| MetricResult {
ops,
cost,
iterations,
proven,
})
}
fn build_sat_guidance(task: &PackedTask, forgos: &[(usize, f64)]) -> SatGuidance {
use std::collections::HashMap;
let mut collect_op: HashMap<usize, usize> = 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.insert(i, oi);
}
}
}
let mut prefs = Vec::new();
for (i, (_, weight)) in forgos.iter().enumerate() {
let Some(&oi) = collect_op.get(&i) else {
continue;
};
let phi: Vec<u32> = task
.pre_pos
.slice(oi)
.iter()
.copied()
.filter(|&f| {
!task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
})
.collect();
if !phi.is_empty() {
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 mut collect_op: HashMap<usize, usize> = 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.insert(i, oi);
}
}
}
let mut value: HashMap<u32, i64> = HashMap::new();
for (i, (_, weight)) in forgos.iter().enumerate() {
let Some(&oi) = collect_op.get(&i) else {
continue;
};
let w = (*weight).round().max(1.0) as i64;
for &f in task.pre_pos.slice(oi) {
if task.fact_names[f as usize]
.to_ascii_uppercase()
.starts_with("(P3")
{
continue;
}
*value.entry(f).or_insert(0) += w;
}
}
if value.is_empty() {
return Vec::new();
}
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];
for ce in task.cond.slice(oi) {
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
}