use std::time::{Duration, Instant};
use crate::hash::{FxHashMap, FxHashSet};
use crate::packed::PackedTask;
use crate::partition::{merge_at, merge_with_neighbor, Subgoal};
use crate::pddl3::{apply_tail, plan_cost, PhaseTail};
use crate::search::{solve_subgoal_bounded, solve_subgoal_guided, SatGuidance, SearchCfg};
pub struct EspcResult {
pub ops: Vec<usize>,
pub cost: f64,
pub iterations: usize,
}
pub struct EspcPartition {
pub comps: Vec<Subgoal>,
pub tail: PhaseTail,
pub assoc: FxHashMap<u32, Vec<u32>>,
}
enum ComposeErr {
Conflict(usize, Option<usize>),
Budget,
Invalid,
}
fn env_i64(key: &str, default: i64) -> i64 {
std::env::var(key)
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(default)
}
fn exhausted(pool: i64, wall: Option<Instant>) -> bool {
pool <= 0 || wall.is_some_and(|d| Instant::now() >= d)
}
fn capped_cfg(cfg: SearchCfg, pool: i64) -> SearchCfg {
SearchCfg {
max_eval: cfg.max_eval.min(pool.max(1) as usize),
..cfg
}
}
#[allow(clippy::too_many_arguments)]
fn compose_once(
task: &PackedTask,
sat: &SatGuidance,
part: &EspcPartition,
threads: usize,
cfg: SearchCfg,
pool: &mut i64,
wall: Option<Instant>,
) -> Result<Vec<usize>, ComposeErr> {
let (comps, tail) = (&part.comps, &part.tail);
let mut state = task.initial();
let mut plan: Vec<usize> = Vec::new();
let mut done = vec![false; comps.len()];
for i in 0..comps.len() {
if i > 0 && exhausted(*pool, wall) {
return Err(ComposeErr::Budget);
}
if task.goal_met_with(&state, &comps[i].pos, &comps[i].num) {
done[i] = true;
continue; }
let protected: FxHashSet<u32> = (0..i)
.filter(|&j| done[j])
.flat_map(|j| comps[j].pos.iter().copied())
.collect();
let forbidden: Vec<bool> = if protected.is_empty() {
Vec::new()
} else {
(0..task.n_ops)
.map(|oi| task.del.slice(oi).iter().any(|f| protected.contains(f)))
.collect()
};
let locked = |d: u32| {
sat.deadline
.iter()
.any(|&(m, dd, _)| dd == d && crate::bitset::test(&state.bits, m as usize))
};
let mut extra: Vec<u32> = comps[i]
.pos
.iter()
.flat_map(|f| part.assoc.get(f).map(|v| v.as_slice()).unwrap_or(&[]))
.copied()
.filter(|&d| {
!crate::bitset::test(&state.bits, d as usize)
&& !comps[i].pos.contains(&d)
&& !locked(d)
})
.collect();
extra.sort_unstable();
extra.dedup();
let enriched = (!extra.is_empty()).then(|| {
let mut g = comps[i].pos.clone();
g.extend(extra);
g
});
let guided = |goal: &[u32], forb: &[bool], pool: &mut i64| {
let (ops, evaluated) = solve_subgoal_guided(
task,
&state,
goal,
&comps[i].num,
forb,
threads,
capped_cfg(cfg, *pool),
Some(sat),
);
*pool -= evaluated as i64;
ops
};
let solved = enriched
.and_then(|g| guided(&g, &forbidden, pool))
.or_else(|| guided(&comps[i].pos, &forbidden, pool))
.or_else(|| {
if forbidden.is_empty() {
None
} else {
guided(&comps[i].pos, &[], pool)
}
});
let Some(ops) = solved else {
return Err(ComposeErr::Conflict(i, None));
};
let mut ns = state.clone();
for &oi in &ops {
ns = task.apply(oi, &ns);
}
if let Some(j) =
(0..i).find(|&j| done[j] && !task.goal_met_with(&ns, &comps[j].pos, &comps[j].num))
{
return Err(ComposeErr::Conflict(i, Some(j)));
}
state = ns;
plan.extend(ops);
done[i] = true;
}
let tail_ops = apply_tail(task, &mut state, tail).ok_or(ComposeErr::Invalid)?;
plan.extend(tail_ops);
if !task.goal_met_with(&state, &task.goal_pos, &task.goal_num) {
return Err(ComposeErr::Invalid);
}
Ok(plan)
}
#[allow(clippy::too_many_arguments)]
fn solve_partitioned(
task: &PackedTask,
cost_fluent: usize,
sat: &SatGuidance,
part: &mut EspcPartition,
threads: usize,
cfg: SearchCfg,
pool: &mut i64,
wall: Option<Instant>,
) -> Result<(Vec<usize>, f64), ComposeErr> {
loop {
match compose_once(task, sat, part, threads, cfg, pool, wall) {
Ok(plan) => {
let cost = plan_cost(task, &plan, cost_fluent);
return Ok((plan, cost));
}
Err(ComposeErr::Conflict(i, j)) => {
if part.comps.len() <= 1 {
return Err(ComposeErr::Invalid);
}
match j {
Some(j) => merge_at(&mut part.comps, i, j),
None => merge_with_neighbor(&mut part.comps, i),
};
}
Err(e) => return Err(e),
}
}
}
fn measure_violation(
task: &PackedTask,
ops: &[usize],
base: &[(u32, u32, i64)],
) -> FxHashMap<u32, i64> {
let mut s = task.initial();
for &oi in ops {
s = task.apply(oi, &s);
}
let mut v: FxHashMap<u32, i64> = FxHashMap::default();
for &(m, d, _) in base {
if crate::bitset::test(&s.bits, m as usize) && !crate::bitset::test(&s.bits, d as usize) {
*v.entry(m).or_insert(0) += 1;
}
}
v
}
#[allow(clippy::too_many_arguments)]
fn solve_under_penalties(
task: &PackedTask,
cost_fluent: usize,
sat: &SatGuidance,
threads: usize,
cfg: SearchCfg,
pool: &mut i64,
wall: Option<Instant>,
bound0: f64,
) -> Option<(Vec<usize>, f64)> {
const INNER_MAX: usize = 200;
let init = task.initial();
let mut bound = bound0;
let mut best: Option<(Vec<usize>, f64)> = None;
for i in 0..INNER_MAX {
if i > 0 && exhausted(*pool, wall) {
break;
}
let (opt, evaluated, _capped) = solve_subgoal_bounded(
task,
&init,
&task.goal_pos,
&task.goal_num,
cost_fluent,
bound,
threads,
capped_cfg(cfg, *pool),
Some(sat),
);
*pool -= evaluated as i64;
match opt {
Some(ops) => {
let cost = plan_cost(task, &ops, cost_fluent);
best = Some((ops, cost));
if cost <= 0.0 {
break;
}
bound = cost; }
None => break,
}
}
best
}
pub fn espc_optimize(
task: &PackedTask,
cost_fluent: usize,
sat: &mut SatGuidance,
seed: Option<(Vec<usize>, f64)>,
part: Option<EspcPartition>,
threads: usize,
cfg: SearchCfg,
) -> Option<EspcResult> {
let base: Vec<(u32, u32, i64)> = sat.deadline.clone();
if base.is_empty() {
return seed.map(|(ops, cost)| EspcResult {
ops,
cost,
iterations: 0,
});
}
let mut part = part.filter(|p| p.comps.len() >= 2 && std::env::var("FF_ESPC_MONO").is_err());
let lambda0 = env_i64("FF_ESPC_LAMBDA0", 0).max(0);
let rate0 = env_i64("FF_ESPC_RATE", 20).max(1);
let outer_default = if part.is_some() { 64 } else { 16 };
let outer_max = env_i64("FF_ESPC_OUTER", outer_default).max(1) as usize;
let k_bump = env_i64("FF_ESPC_K", 2).max(1); let stall_max = env_i64("FF_ESPC_STALL", 4).max(1) as usize;
let eval_budget = env_i64("FF_ESPC_EVAL_BUDGET", 6_000_000).max(1);
let wall: Option<Instant> = std::env::var("FF_ESPC_TIME_MS")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.map(|ms| Instant::now() + Duration::from_millis(ms.max(0) as u64));
let debug = std::env::var("FF_RES_DEBUG").is_ok();
let mut lambda: FxHashMap<u32, i64> = FxHashMap::default();
for &(m, _, _) in &base {
lambda.entry(m).or_insert(lambda0);
}
sat.deadline_weight = 1;
if debug {
match &part {
Some(p) => eprintln!(
"[ESPC] partitioned: {} component(s), tail closes {} preference(s)",
p.comps.len(),
p.tail.prefs.len()
),
None => eprintln!("[ESPC] monolithic"),
}
}
let mut best = seed;
let mut rate = rate0;
let mut consec = 0i64;
let mut stall = 0usize;
let mut iterations = 0usize;
let mut loop_pool: i64 = if part.is_some() {
eval_budget / 2
} else {
eval_budget
};
for outer in 0..outer_max {
if outer > 0 && exhausted(loop_pool, wall) {
break;
}
iterations += 1;
for (pair, b) in sat.deadline.iter_mut().zip(&base) {
let lam = *lambda.get(&b.0).unwrap_or(&0);
pair.2 = lam.saturating_mul(b.2);
}
let solved = match part.as_mut() {
Some(p) => {
match solve_partitioned(
task,
cost_fluent,
sat,
p,
threads,
cfg,
&mut loop_pool,
wall,
) {
Ok(r) => Some(r),
Err(ComposeErr::Budget) => None,
Err(_) => solve_under_penalties(
task,
cost_fluent,
sat,
threads,
cfg,
&mut loop_pool,
wall,
f64::INFINITY,
),
}
}
None => solve_under_penalties(
task,
cost_fluent,
sat,
threads,
cfg,
&mut loop_pool,
wall,
f64::INFINITY,
),
};
let Some((ops, cost)) = solved else {
break; };
let viol = measure_violation(task, &ops, &base);
let total_v: i64 = viol.values().sum();
let improved = best.as_ref().map_or(true, |(_, c)| cost < *c - 1e-9);
if improved {
best = Some((ops, cost));
stall = 0;
consec = 0;
} else {
stall += 1;
consec += 1;
}
if debug {
let comps = part.as_ref().map_or(1, |p| p.comps.len());
eprintln!(
"[ESPC] iter {outer}: cost={cost} violations={total_v} best={} rate={rate} comps={comps}",
best.as_ref().map(|(_, c)| *c).unwrap_or(f64::INFINITY)
);
}
if total_v == 0 {
break; }
if stall >= stall_max {
break; }
for (&m, &v) in &viol {
if v > 0 {
let e = lambda.entry(m).or_insert(lambda0);
*e = e.saturating_add(rate.saturating_mul(v));
}
}
if consec >= k_bump {
rate = rate.saturating_mul(2);
consec = 0;
}
}
if part.is_some() {
let mut polish_pool: i64 = (eval_budget - eval_budget / 2) + loop_pool.max(0);
if !exhausted(polish_pool, wall) {
let bound0 = best.as_ref().map_or(f64::INFINITY, |(_, c)| *c);
if bound0 > 0.0 {
if let Some((ops, cost)) = solve_under_penalties(
task,
cost_fluent,
sat,
threads,
cfg,
&mut polish_pool,
wall,
bound0,
) {
if best.as_ref().map_or(true, |(_, c)| cost < *c - 1e-9) {
if debug {
eprintln!("[ESPC] polish: {bound0} -> {cost}");
}
best = Some((ops, cost));
}
}
}
}
}
best.map(|(ops, cost)| EspcResult {
ops,
cost,
iterations,
})
}