use std::cmp::Reverse;
use std::collections::{BinaryHeap, VecDeque};
use crate::hash::FxHashSet;
use crate::heuristic::{relaxed_costed, relaxed_helpful, relaxed_to, Scratch};
use crate::packed::{PackedTask, State, StateKey};
use crate::par;
use crate::types::NumPre;
const BATCH: usize = 256;
pub const DEFAULT_MAX_EVAL: usize = 5_000_000;
const WEIGHT_SCALE: f64 = 256.0;
const NODE_CAP_TARGET_BYTES: usize = 8 << 30;
pub(crate) fn node_cap_for(task: &PackedTask) -> usize {
if let Ok(v) = std::env::var("FF_SEARCH_NODE_CAP") {
if let Ok(n) = v.trim().parse::<usize>() {
return if n == 0 { usize::MAX } else { n };
}
}
let per_node = 2 * task.words * 8 + task.fv0.len() * 8 + task.fdef0.len() + 96;
NODE_CAP_TARGET_BYTES / per_node.max(1)
}
#[derive(Clone, Copy, Debug)]
pub struct SearchCfg {
pub w_g: i64,
pub w_h: i64,
pub max_eval: usize,
pub w_c: f64,
pub h_cost: Option<usize>,
pub anytime: bool,
pub g_bound: usize,
}
impl Default for SearchCfg {
fn default() -> Self {
SearchCfg::from_weights(1.0, 5.0, None)
}
}
impl SearchCfg {
pub fn from_weights(weight_g: f64, weight_h: f64, max_eval: Option<usize>) -> Self {
let san = |w: f64, default: f64| {
if w.is_finite() && w >= 0.0 {
w.min(1e9)
} else {
default
}
};
let mut w_g = (san(weight_g, 1.0) * WEIGHT_SCALE).round() as i64;
let mut w_h = (san(weight_h, 5.0) * WEIGHT_SCALE).round() as i64;
if w_g == 0 && w_h == 0 {
w_g = WEIGHT_SCALE as i64;
w_h = (5.0 * WEIGHT_SCALE) as i64;
}
SearchCfg {
w_g,
w_h,
max_eval: max_eval.unwrap_or(DEFAULT_MAX_EVAL),
w_c: 0.0,
h_cost: None,
anytime: false,
g_bound: usize::MAX,
}
}
pub fn with_cost_h(mut self, cost_fluent: usize) -> Self {
self.h_cost = Some(cost_fluent);
self
}
pub fn with_cost_weight(mut self, w_c: f64) -> Self {
self.w_c = if w_c.is_finite() && w_c > 0.0 {
w_c.min(1e9)
} else {
0.0
};
self
}
}
pub enum PlanResult {
Plan {
ops: Vec<usize>,
advance: Vec<i32>,
evaluated: usize,
max_g: usize,
},
Unsolvable {
evaluated: usize,
capped: bool, },
}
struct Node {
state: State,
father: usize,
op: usize,
g: usize,
}
pub struct PrefPhi {
pub disjuncts: Vec<(Vec<u32>, Vec<NumPre>)>,
}
impl PrefPhi {
#[inline]
pub fn holds(&self, s: &State) -> bool {
self.disjuncts.iter().any(|(pos, num)| {
pos.iter()
.all(|&f| crate::bitset::test(&s.bits, f as usize))
&& num
.iter()
.all(|np| crate::types::eval_numpre(np, &s.fv, &s.fdef).unwrap_or(false))
})
}
}
pub struct ClosureCost {
pub prefs: Vec<(f64, PrefPhi)>,
}
impl ClosureCost {
pub fn cost(&self, s: &State) -> f64 {
self.prefs
.iter()
.filter(|(_, phi)| !phi.holds(s))
.map(|(w, _)| w)
.sum()
}
}
pub struct SatGuidance {
pub prefs: Vec<(PrefPhi, i64)>,
pub res: Vec<crate::resource::ResourceVar>,
pub res_weight: i64,
pub res_thresh: i64,
pub deadline: Vec<(u32, u32, i64)>,
pub deadline_weight: i64,
}
impl SatGuidance {
fn forgone(&self, s: &State) -> i64 {
let mut pen = 0;
for (phi, p) in &self.prefs {
if !phi.holds(s) {
pen += *p;
}
}
pen
}
fn penalty(&self, s: &State) -> i64 {
let mut pen = self.forgone(s);
if self.res_weight != 0 {
for r in &self.res {
let over = (r.occupancy(&s.bits) as i64 - self.res_thresh).max(0);
pen += self.res_weight * over * over;
}
}
if self.deadline_weight != 0 {
for &(trigger, deliverable, val) in &self.deadline {
if crate::bitset::test(&s.bits, trigger as usize)
&& !crate::bitset::test(&s.bits, deliverable as usize)
{
pen += self.deadline_weight * val;
}
}
}
pen
}
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn search_from(
task: &PackedTask,
start: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
cost_fluent: Option<usize>,
cost_bound: f64,
threads: usize,
cfg: SearchCfg,
forbidden: &[bool],
sat: Option<&SatGuidance>,
closure: Option<&ClosureCost>,
) -> PlanResult {
let batch = BATCH;
let node_cap = node_cap_for(task);
let init = start.clone();
if relaxed_to(
task,
&mut Scratch::new(task),
&init.bits,
&init.fv,
&init.fdef,
goal_pos,
goal_num,
)
.is_none()
{
return PlanResult::Unsolvable {
evaluated: 1,
capped: false,
};
}
let mut nodes: Vec<Node> = vec![Node {
state: init.clone(),
father: usize::MAX,
op: usize::MAX,
g: 0,
}];
let mut heap: BinaryHeap<Reverse<(i64, usize)>> = BinaryHeap::new();
heap.push(Reverse((0, 0))); let mut visited: FxHashSet<StateKey> = FxHashSet::default();
visited.insert(task.state_key_with_cost(&init, cost_fluent));
let mut evaluated = 0usize;
let mut best = i32::MAX;
let mut advance: Vec<i32> = Vec::new();
let mut max_g = 0usize;
let mut cost_bound = cost_bound;
let mut best_acc: Option<usize> = None;
while !heap.is_empty() {
let mut popped: Vec<usize> = Vec::with_capacity(batch);
for _ in 0..batch {
match heap.pop() {
Some(Reverse((_, ni))) => popped.push(ni),
None => break,
}
}
for &ni in &popped {
max_g = max_g.max(nodes[ni].g);
if !task.goal_met_with(&nodes[ni].state, goal_pos, goal_num) {
continue;
}
if cfg.anytime {
let s = &nodes[ni].state;
let g = cost_fluent
.map(|cf| if s.fdef[cf] { s.fv[cf] } else { 0.0 })
.unwrap_or(0.0);
let eff = g + closure.map_or(0.0, |cl| cl.cost(s));
if eff < cost_bound {
best_acc = Some(ni);
cost_bound = eff;
if eff <= 0.0 {
return PlanResult::Plan {
ops: reconstruct(&nodes, ni),
advance,
evaluated,
max_g,
};
}
}
continue;
}
if closure.map_or(true, |cl| {
let s = &nodes[ni].state;
let g = cost_fluent
.map(|cf| if s.fdef[cf] { s.fv[cf] } else { 0.0 })
.unwrap_or(0.0);
g + cl.cost(s) < cost_bound
}) {
return PlanResult::Plan {
ops: reconstruct(&nodes, ni),
advance,
evaluated,
max_g,
};
}
}
if cfg.anytime {
if let Some(cf) = cost_fluent {
let bound_now = cost_bound;
popped.retain(|&ni| {
let s = &nodes[ni].state;
!(s.fdef[cf] && s.fv[cf] >= bound_now)
});
if popped.is_empty() {
continue;
}
}
}
let hs: Vec<Option<i32>> = par::par_map_with(
&popped,
threads,
|| Scratch::new(task),
|sc, &ni| {
let s = &nodes[ni].state;
match cfg.h_cost {
Some(hcf) => {
relaxed_costed(task, sc, &s.bits, &s.fv, &s.fdef, goal_pos, goal_num, hcf)
}
None => relaxed_to(task, sc, &s.bits, &s.fv, &s.fdef, goal_pos, goal_num),
}
},
);
evaluated += popped.len();
if evaluated > cfg.max_eval || nodes.len() > node_cap {
if let Some(ni) = best_acc {
return PlanResult::Plan {
ops: reconstruct(&nodes, ni),
advance,
evaluated,
max_g,
};
}
return PlanResult::Unsolvable {
evaluated,
capped: true,
};
}
for h in hs.iter().flatten() {
if *h < best {
best = *h;
advance.push(*h);
}
}
let live: Vec<(usize, i32)> = popped
.iter()
.zip(hs.iter())
.filter_map(|(&ni, h)| h.map(|h| (ni, h)))
.collect();
let cand_chunks: Vec<Vec<(usize, usize, State, StateKey, i32)>> =
par::par_map(&live, threads, |&(ni, ph)| {
let st = &nodes[ni].state;
let mut v = Vec::new();
for oi in 0..task.n_ops {
if forbidden.get(oi).copied().unwrap_or(false) {
continue;
}
if task.op_applicable(oi, st) {
let ns = task.apply(oi, st);
if let Some(cf) = cost_fluent {
if ns.fdef[cf] && ns.fv[cf] >= cost_bound {
continue; }
}
let k = task.state_key_with_cost(&ns, cost_fluent);
v.push((ni, oi, ns, k, ph));
}
}
v
});
for chunk in cand_chunks {
for (pi, oi, s, k, ph) in chunk {
let g = nodes[pi].g + 1;
if g >= cfg.g_bound {
continue; }
if visited.insert(k) {
let sat_pen = sat.map(|sg| sg.penalty(&s)).unwrap_or(0);
let cost_term = if cfg.w_c != 0.0 {
let c = cost_fluent
.map(|cf| if s.fdef[cf] { s.fv[cf] } else { 0.0 })
.unwrap_or(0.0);
(cfg.w_c * c * WEIGHT_SCALE).round() as i64
} else {
0
};
let idx = nodes.len();
nodes.push(Node {
state: s,
father: pi,
op: oi,
g,
});
heap.push(Reverse((
cfg.w_g * g as i64 + cfg.w_h * ph as i64 + sat_pen + cost_term,
idx,
)));
}
}
}
}
if let Some(ni) = best_acc {
return PlanResult::Plan {
ops: reconstruct(&nodes, ni),
advance,
evaluated,
max_g,
};
}
PlanResult::Unsolvable {
evaluated,
capped: false,
}
}
fn reconstruct(nodes: &[Node], mut ni: usize) -> Vec<usize> {
let mut ops = Vec::new();
while nodes[ni].father != usize::MAX {
ops.push(nodes[ni].op);
ni = nodes[ni].father;
}
ops.reverse();
ops
}
pub fn search(task: &PackedTask, threads: usize, cfg: SearchCfg) -> PlanResult {
search_from(
task,
&task.initial(),
&task.goal_pos,
&task.goal_num,
None,
f64::INFINITY,
threads,
cfg,
&[],
None,
None,
)
}
pub struct PlanOutcome {
pub ops: Option<Vec<usize>>,
pub evaluated: usize,
pub ehc_fell_back: bool,
}
pub fn plan(task: &PackedTask, threads: usize, cfg: SearchCfg, ehc_first: bool) -> PlanOutcome {
plan_avoiding(task, threads, cfg, ehc_first, &[])
}
pub fn plan_avoiding(
task: &PackedTask,
threads: usize,
cfg: SearchCfg,
ehc_first: bool,
forbidden: &[bool],
) -> PlanOutcome {
if ehc_first {
if let Some((ops, evaluated)) = ehc(task, forbidden) {
return PlanOutcome {
ops: Some(ops),
evaluated,
ehc_fell_back: false,
};
}
if std::env::var("FF_NO_LAMA").is_err() {
const LAMA_CAP: usize = 400_000;
if let Some((ops, evaluated)) =
crate::lama::search(task, threads, LAMA_CAP.min(cfg.max_eval), forbidden)
{
return PlanOutcome {
ops: Some(ops),
evaluated,
ehc_fell_back: true,
};
}
}
}
let (ops, evaluated) = match search_from(
task,
&task.initial(),
&task.goal_pos,
&task.goal_num,
None,
f64::INFINITY,
threads,
cfg,
forbidden,
None,
None,
) {
PlanResult::Plan { ops, evaluated, .. } => (Some(ops), evaluated),
PlanResult::Unsolvable { evaluated, .. } => (None, evaluated),
};
PlanOutcome {
ops,
evaluated,
ehc_fell_back: ehc_first,
}
}
fn ehc(task: &PackedTask, forbidden: &[bool]) -> Option<(Vec<usize>, usize)> {
let init = task.initial();
let mut sc = Scratch::new(task);
let (mut cur_h, _) = relaxed_helpful(
task,
&mut sc,
&init.bits,
&init.fv,
&init.fdef,
&task.goal_pos,
&task.goal_num,
)?;
let mut evaluated = 1usize;
if task.goal_met(&init) {
return Some((Vec::new(), evaluated));
}
let total_cap = (200 * task.n_ops).max(30_000);
let mut current = init;
let mut plan: Vec<usize> = Vec::new();
loop {
match bfs_improve(task, &mut sc, ¤t, cur_h, &mut evaluated, forbidden) {
Some((ops, next, next_h)) => {
plan.extend(ops);
current = next;
cur_h = next_h;
if task.goal_met(¤t) {
return Some((plan, evaluated));
}
if evaluated > total_cap {
return None; }
}
None => return None, }
}
}
fn bfs_improve(
task: &PackedTask,
sc: &mut Scratch,
start: &State,
h_start: i32,
evaluated: &mut usize,
forbidden: &[bool],
) -> Option<(Vec<usize>, State, i32)> {
const BFS_CAP: usize = 5_000;
struct N {
state: State,
father: usize,
op: usize,
}
let (_, root_helpful) = relaxed_helpful(
task,
sc,
&start.bits,
&start.fv,
&start.fdef,
&task.goal_pos,
&task.goal_num,
)?;
let mut nodes = vec![N {
state: start.clone(),
father: usize::MAX,
op: usize::MAX,
}];
let mut visited: FxHashSet<StateKey> = FxHashSet::default();
visited.insert(task.state_key(start));
let mut queue: VecDeque<(usize, Vec<u32>)> = VecDeque::new();
queue.push_back((0, root_helpful));
let mut expanded = 0usize;
while let Some((ni, helpful)) = queue.pop_front() {
for &oi in &helpful {
let oi = oi as usize;
if forbidden.get(oi).copied().unwrap_or(false) {
continue;
}
if !task.op_applicable(oi, &nodes[ni].state) {
continue;
}
let ns = task.apply(oi, &nodes[ni].state);
if !visited.insert(task.state_key(&ns)) {
continue;
}
*evaluated += 1;
let (h_ns, helpful_ns) = match relaxed_helpful(
task,
sc,
&ns.bits,
&ns.fv,
&ns.fdef,
&task.goal_pos,
&task.goal_num,
) {
Some(x) => x,
None => continue, };
let idx = nodes.len();
nodes.push(N {
state: ns.clone(),
father: ni,
op: oi,
});
if h_ns < h_start {
let mut ops = Vec::new();
let mut c = idx;
while nodes[c].father != usize::MAX {
ops.push(nodes[c].op);
c = nodes[c].father;
}
ops.reverse();
return Some((ops, ns, h_ns));
}
expanded += 1;
if expanded > BFS_CAP {
return None;
}
queue.push_back((idx, helpful_ns));
}
}
None
}
pub fn solve_subgoal(
task: &PackedTask,
start: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
threads: usize,
cfg: SearchCfg,
) -> Option<Vec<usize>> {
solve_subgoal_avoiding(task, start, goal_pos, goal_num, &[], threads, cfg)
}
#[allow(clippy::too_many_arguments)]
pub fn solve_subgoal_avoiding(
task: &PackedTask,
start: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
forbidden: &[bool],
threads: usize,
cfg: SearchCfg,
) -> Option<Vec<usize>> {
match search_from(
task,
start,
goal_pos,
goal_num,
None,
f64::INFINITY,
threads,
cfg,
forbidden,
None,
None,
) {
PlanResult::Plan { ops, .. } => Some(ops),
PlanResult::Unsolvable { .. } => None,
}
}
#[allow(clippy::too_many_arguments)]
pub fn solve_subgoal_bounded(
task: &PackedTask,
start: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
cost_fluent: usize,
bound: f64,
threads: usize,
cfg: SearchCfg,
sat: Option<&SatGuidance>,
) -> (Option<Vec<usize>>, usize, bool) {
match search_from(
task,
start,
goal_pos,
goal_num,
Some(cost_fluent),
bound,
threads,
cfg,
&[],
sat,
None,
) {
PlanResult::Plan { ops, evaluated, .. } => (Some(ops), evaluated, false),
PlanResult::Unsolvable { evaluated, capped } => (None, evaluated, capped),
}
}
#[allow(clippy::too_many_arguments)]
pub fn solve_closure_bounded(
task: &PackedTask,
goal_pos: &[u32],
goal_num: &[NumPre],
cost_fluent: usize,
bound: f64,
closure: &ClosureCost,
forbidden: &[bool],
threads: usize,
cfg: SearchCfg,
sat: Option<&SatGuidance>,
) -> (Option<Vec<usize>>, usize, bool) {
match search_from(
task,
&task.initial(),
goal_pos,
goal_num,
Some(cost_fluent),
bound,
threads,
cfg,
forbidden,
sat,
Some(closure),
) {
PlanResult::Plan { ops, evaluated, .. } => (Some(ops), evaluated, false),
PlanResult::Unsolvable { evaluated, capped } => (None, evaluated, capped),
}
}
#[allow(clippy::too_many_arguments)]
pub fn solve_subgoal_guided(
task: &PackedTask,
start: &State,
goal_pos: &[u32],
goal_num: &[NumPre],
forbidden: &[bool],
threads: usize,
cfg: SearchCfg,
sat: Option<&SatGuidance>,
) -> (Option<Vec<usize>>, usize) {
match search_from(
task,
start,
goal_pos,
goal_num,
None,
f64::INFINITY,
threads,
cfg,
forbidden,
sat,
None,
) {
PlanResult::Plan { ops, evaluated, .. } => (Some(ops), evaluated),
PlanResult::Unsolvable { evaluated, .. } => (None, evaluated),
}
}