diff --git a/planning/planners/src/bin/lcp.rs b/planning/planners/src/bin/lcp.rs index bb4d52ad..de115033 100644 --- a/planning/planners/src/bin/lcp.rs +++ b/planning/planners/src/bin/lcp.rs @@ -3,7 +3,7 @@ use aries::core::state::Domains; use aries::utils::input::Input; use aries_planners::solver::{format_plan, solve, SolverResult}; use aries_planners::solver::{Metric, Strat}; -use aries_planning::chronicles::analysis::hierarchical_is_non_recursive; +use aries_planning::chronicles::analysis::hierarchy::hierarchical_is_non_recursive; use aries_planning::chronicles::FiniteProblem; use aries_planning::parsing::pddl::{find_domain_of, parse_pddl_domain, parse_pddl_problem, PddlFeature}; use aries_planning::parsing::pddl_to_chronicles; diff --git a/planning/planners/src/encode.rs b/planning/planners/src/encode.rs index 46ef6b17..b230a3bd 100644 --- a/planning/planners/src/encode.rs +++ b/planning/planners/src/encode.rs @@ -1,8 +1,10 @@ //! Functions whose purpose is to encode a planning problem (represented with chronicles) //! into a combinatorial problem from Aries core. +mod fluent_hierarchy; mod symmetry; +use crate::encode::symmetry::SYMMETRY_BREAKING; use crate::encoding::*; use crate::solver::{init_solver, Metric}; use crate::Model; @@ -21,7 +23,6 @@ use env_param::EnvParam; use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::ptr; -use crate::encode::symmetry::SYMMETRY_BREAKING; /// Parameter that activates the debug view of the resource constraints. /// The parameter is loaded from the environment variable `ARIES_RESOURCE_CONSTRAINT_DEBUG`. @@ -391,7 +392,6 @@ fn enforce_refinement(t: TaskRef, supporters: Vec, model: &mut Model) { } } - /// Encode a metric in the problem and returns an integer that should minimized in order to optimize the metric. pub fn add_metric(pb: &FiniteProblem, model: &mut Model, metric: Metric) -> IAtom { match metric { diff --git a/planning/planners/src/encode/symmetry.rs b/planning/planners/src/encode/symmetry.rs index 6b39c837..cbd56c00 100644 --- a/planning/planners/src/encode/symmetry.rs +++ b/planning/planners/src/encode/symmetry.rs @@ -1,8 +1,12 @@ -use crate::encoding::{ChronicleId, CondID, Encoding, Tag}; +use crate::encode::analysis; +use crate::encode::fluent_hierarchy::hierarchy; +use crate::encoding::{conditions, ChronicleId, CondID, EffID, Encoding, Tag}; use crate::fmt::format_partial_name; +use analysis::CausalSupport; use aries::core::Lit; use aries::model::extensions::AssignmentExt; use aries::model::lang::expr::{and, f_leq, implies, or}; +use aries_planning::chronicles::analysis::TemplateCondID; use aries_planning::chronicles::{ChronicleOrigin, FiniteProblem}; use env_param::EnvParam; use itertools::Itertools; @@ -13,7 +17,8 @@ use crate::Model; /// Parameter that defines the symmetry breaking strategy to use. /// The value of this parameter is loaded from the environment variable `ARIES_LCP_SYMMETRY_BREAKING`. /// Possible values are `none` and `simple` (default). -pub static SYMMETRY_BREAKING: EnvParam = EnvParam::new("ARIES_LCP_SYMMETRY_BREAKING", "simple"); +pub static SYMMETRY_BREAKING: EnvParam = EnvParam::new("ARIES_LCP_SYMMETRY_BREAKING", "psp"); +pub static USELESS_SUPPORTS: EnvParam = EnvParam::new("ARIES_USELESS_SUPPORTS", "true"); /// The type of symmetry breaking to apply to problems. #[derive(Copy, Clone)] @@ -70,6 +75,23 @@ pub fn add_symmetry_breaking(pb: &FiniteProblem, model: &mut Model, tpe: Symmetr } fn add_plan_space_symmetry_breaking(pb: &FiniteProblem, model: &mut Model, encoding: &Encoding) { + let discard_useless_supports = USELESS_SUPPORTS.get(); + let template_id = |instance_id: usize| match pb.chronicles[instance_id].origin { + ChronicleOrigin::FreeAction { template_id, .. } => Some(template_id), + _ => None, + }; + let is_primary_support = |c: CondID, eff: EffID| { + let Some(c_template) = template_id(c.instance_id) else { + return true; + }; + let Some(e_template) = template_id(eff.instance_id) else { + return true; + }; + let causal = CausalSupport::new(e_template, eff.eff_id, c_template, c.cond_id); + // return true if the potential causal link is not flagged as useless + !pb.meta.detrimental_supports.contains(&causal) + }; + struct ActionOrigin { template: usize, gen: usize, @@ -97,8 +119,21 @@ fn add_plan_space_symmetry_breaking(pb: &FiniteProblem, model: &mut Model, encod lit: Lit, } type TemplateID = usize; + let templates = pb + .chronicles + .iter() + .filter_map(|c| match c.origin { + ChronicleOrigin::FreeAction { template_id, .. } => Some(template_id), + _ => None, + }) + .sorted() + .dedup() + .collect_vec(); let mut causal_link: HashMap<(ChronicleId, CondID), Lit> = Default::default(); let mut conds_by_templates: HashMap> = Default::default(); + for template in &templates { + conds_by_templates.insert(*template, HashSet::new()); + } for &(k, v) in &encoding.tags { let Tag::Support(cond, eff) = k else { panic!("Unsupported tag: {k:?}"); @@ -111,8 +146,11 @@ fn add_plan_space_symmetry_breaking(pb: &FiniteProblem, model: &mut Model, encod let ChronicleOrigin::FreeAction { template_id, .. } = ch.origin else { continue; }; + if discard_useless_supports && !is_primary_support(cond, eff) { + continue; // remove non-primary supports + } // record that this template may contribute to this condition - conds_by_templates.entry(template_id).or_default().insert(cond); + conds_by_templates.get_mut(&template_id).unwrap().insert(cond); // non-optional literal that is true iff the causal link is active let link_active = model.reify(and([v, model.presence_literal(v.variable())])); // list of outgoing causal links of the supporting action @@ -123,14 +161,15 @@ fn add_plan_space_symmetry_breaking(pb: &FiniteProblem, model: &mut Model, encod conds_by_templates.into_iter().map(|(k, v)| (k, sort(v))).collect(); let supports = |ch: ChronicleId, cond: CondID| causal_link.get(&(ch, cond)).copied().unwrap_or(Lit::FALSE); - for (template_id, conditions) in &conds_by_templates { + for template_id in &templates { + let conditions = &conds_by_templates[template_id]; let instances: Vec<_> = actions .iter() .filter_map(|(id, orig)| if orig.template == *template_id { Some(id) } else { None }) .sorted() .collect(); - if let Some(ch) = instances.get(0) { + if let Some(ch) = instances.first() { let ch = &pb.chronicles[**ch]; let s = format_partial_name(&ch.chronicle.name, model).unwrap(); println!("{template_id} {s} ({})", instances.len()); @@ -141,11 +180,10 @@ fn add_plan_space_symmetry_breaking(pb: &FiniteProblem, model: &mut Model, encod } for (i, instance) in instances.iter().copied().enumerate() { + let mut clause = Vec::with_capacity(64); if i > 0 { let prev = instances[i - 1]; - let mut clause = Vec::with_capacity(64); - // the chronicle is allowed to support a condition only if the previous chronicle // supports a condition at an earlier level for (cond_index, cond) in conditions.iter().enumerate() { @@ -157,19 +195,20 @@ fn add_plan_space_symmetry_breaking(pb: &FiniteProblem, model: &mut Model, encod } model.enforce(or(clause.as_slice()), []); } - - clause.clear(); - // enforce that a chronicle be present only if it supports at least one condition - clause.push(!pb.chronicles[*instance].chronicle.presence); - for cond in conditions { - clause.push(supports(*instance, *cond)) - } - model.enforce(or(clause.as_slice()), []); } + clause.clear(); + // enforce that a chronicle be present only if it supports at least one condition + clause.push(!pb.chronicles[*instance].chronicle.presence); + for cond in conditions { + clause.push(supports(*instance, *cond)) + } + model.enforce(or(clause.as_slice()), []); } } - println!("\n================\n"); + // println!("\n================\n"); + // hierarchy(pb); + // println!("\n================\n"); // std::process::exit(1) } diff --git a/planning/planners/src/solver.rs b/planning/planners/src/solver.rs index 82b2a72a..83c303db 100644 --- a/planning/planners/src/solver.rs +++ b/planning/planners/src/solver.rs @@ -96,6 +96,8 @@ pub fn solve( Printer::print_problem(&base_problem); } + let metadata = Arc::new(analysis::analyse(&base_problem)); + let mut best_cost = INT_CST_MAX + 1; let start = Instant::now(); @@ -106,6 +108,7 @@ pub fn solve( horizon: base_problem.context.horizon(), makespan_ub: base_problem.context.makespan_ub(), chronicles: base_problem.chronicles.clone(), + meta: metadata.clone(), }; let depth_string = if depth == u32::MAX { "∞".to_string() @@ -286,7 +289,17 @@ fn causal_brancher(problem: Arc, encoding: Arc) -> Bran let causal = ManualCausalSearch::new(problem, encoding); // conflict directed search on tagged literals only - let conflict = Box::new(ConflictBasedBrancher::new(branching_literals)); + let mut conflict = Box::new(ConflictBasedBrancher::new(branching_literals.clone())); + // when possible, set the value of the prefered value of the branching literal + for l in branching_literals { + let var = l.variable(); + if l == Lit::gt(var, 0) { + conflict.set_default_value(var, 1); + } else if l == Lit::leq(var, 0) { + conflict.set_default_value(var, 0); + } + } + // std::process::exit(0); // if all tagged literals are set, fallback to standard activity-based search let act: Box> = diff --git a/planning/planning/src/chronicles/analysis/detrimental_supports.rs b/planning/planning/src/chronicles/analysis/detrimental_supports.rs new file mode 100644 index 00000000..8975a426 --- /dev/null +++ b/planning/planning/src/chronicles/analysis/detrimental_supports.rs @@ -0,0 +1,394 @@ +use crate::chronicles::analysis::is_static; +use crate::chronicles::{EffectOp, Fluent, Problem}; +use aries::core::Lit; +use aries::model::lang::{Atom, SAtom}; +use aries::model::symbols::{SymId, TypedSym}; +use aries::model::types::TypeId; +use itertools::Itertools; +use std::collections::{HashMap, HashSet}; +use std::fmt::Write; + +#[derive(Eq, PartialEq, Copy, Clone, Hash)] +enum Value { + SymType(TypeId), + SymCst(TypedSym), + BoolType, + BoolCst(bool), +} + +impl Value { + pub fn is_constant(&self) -> bool { + match self { + Value::SymCst(_) | Value::BoolCst(_) => true, + Value::SymType(_) | Value::BoolType => false, + } + } + fn write(&self, out: &mut String, pb: &Problem) { + match self { + Value::SymType(t) => { + let tpe = pb.context.model.shape.symbols.types.from_id(*t); + write!(out, "{tpe}").unwrap() + } + Value::SymCst(ts) => { + let sym = pb.context.model.shape.symbols.symbol(ts.sym); + write!(out, "{sym}").unwrap() + } + Value::BoolType => write!(out, "?").unwrap(), + Value::BoolCst(b) => write!(out, "{b}").unwrap(), + } + } +} + +#[derive(Eq, PartialEq, Clone, Hash)] +struct GAtom { + fluent: SymId, + params: Vec, + value: Value, +} + +impl GAtom { + pub fn format(&self, pb: &Problem) -> String { + let mut out = String::with_capacity(64); + let fluent = pb.context.model.shape.symbols.symbol(self.fluent); + write!(out, "{fluent}(").unwrap(); + for (i, arg) in self.params.iter().enumerate() { + if i > 0 { + write!(out, ", ").unwrap() + } + arg.write(&mut out, pb) + } + write!(out, "):"); + self.value.write(&mut out, pb); + + out + } +} + +fn atom_to_gatom(a: impl Into) -> Value { + let a = a.into(); + match a { + Atom::Bool(b) => { + if b == Lit::TRUE { + Value::BoolCst(true) + } else if b == Lit::FALSE { + Value::BoolCst(false) + } else { + Value::BoolType + } + } + Atom::Int(_) => todo!(), + Atom::Fixed(_) => todo!(), + Atom::Sym(SAtom::Cst(tsym)) => Value::SymCst(tsym), + Atom::Sym(SAtom::Var(s)) => Value::SymType(s.tpe), + } +} + +fn value_of(fluent: &Fluent, params: &[SAtom], value: Atom) -> GAtom { + let fluent = fluent.sym; + let params = params.iter().map(|sa| atom_to_gatom(*sa)).collect_vec(); + let value = atom_to_gatom(value); + + GAtom { fluent, params, value } +} + +pub fn find_useless_supports(pb: &Problem) -> HashSet { + let mut useful_values = HashSet::new(); // TODO: extend with goals + + for ch in &pb.templates { + for cond in &ch.chronicle.conditions { + let gval = value_of(&cond.state_var.fluent, &cond.state_var.args, cond.value); + useful_values.insert(gval); + } + } + + println!("Useful values:"); + for v in &useful_values { + println!(" - {}", v.format(pb)); + } + + println!("Continuous fluents: "); + let mut detrimental_conditions = HashSet::new(); + for f in &pb.context.fluents { + if is_static(f.as_ref(), pb) { + continue; + } + gather_detrimental_supports(f.as_ref(), pb, &useful_values, &mut detrimental_conditions) + } + detrimental_conditions +} + +#[allow(unused)] +/// Function to build an operator graph +fn build_graph(pb: &Problem) { + let mut useful_values = HashSet::new(); // TODO: extend with goals + let g = &mut String::new(); + write!(g, "digraph ops {{\n"); + for (i, ch) in pb.templates.iter().enumerate() { + println!("{:?}", ch.label); + write!( + g, + " {i} [shape=\"rectangle\", label=\"{}\"];\n", + ch.label.as_ref().unwrap() + ); + println!(" cond:"); + for cond in &ch.chronicle.conditions { + let gval = value_of(&cond.state_var.fluent, &cond.state_var.args, cond.value); + println!(" - {}", gval.format(pb)); + write!(g, " \"{}\" -> {i};\n", gval.format(pb)); + useful_values.insert(gval); + } + println!(" effs:"); + for eff in &ch.chronicle.effects { + let EffectOp::Assign(value) = eff.operation else { + continue; + }; + let gval = value_of(&eff.state_var.fluent, &eff.state_var.args, value); + println!(" - {}", gval.format(pb)); + write!(g, " {i} -> \"{}\";\n", gval.format(pb)); + } + } + write!(g, "}}"); + std::fs::write("/tmp/graph.dot", g).expect("Unable to write file"); + // std::process::exit(0) + + println!("Useful values:"); + for v in &useful_values { + println!(" - {}", v.format(pb)); + } + + println!("Continuous fluents: "); + let mut detrimental_conditions = HashSet::new(); + for f in &pb.context.fluents { + if is_static(f.as_ref(), pb) { + continue; + } + gather_detrimental_supports(f.as_ref(), pb, &useful_values, &mut detrimental_conditions) + } +} + +#[derive(Hash, Copy, Clone, Eq, PartialEq)] +pub struct TemplateCondID { + /// id of the chronicle template in which the condition occurs + pub template_id: usize, + /// Index of the condition in the template's conditions + pub cond_id: usize, +} +#[derive(Hash, Copy, Clone, Eq, PartialEq)] +pub struct TemplateEffID { + /// id of the chronicle template in which the condition occurs + pub template_id: usize, + /// Index of the effect in the template's effects + pub effect_id: usize, +} + +/// Represents a potential causal support from the effect of a template to the condition of a template +#[derive(Hash, Copy, Clone, Eq, PartialEq)] +pub struct CausalSupport { + supporter: TemplateEffID, + condition: TemplateCondID, +} + +impl CausalSupport { + pub fn new(eff_template: usize, eff_id: usize, cond_template: usize, cond_id: usize) -> Self { + Self { + supporter: TemplateEffID { + template_id: eff_template, + effect_id: eff_id, + }, + condition: TemplateCondID { + template_id: cond_template, + cond_id: cond_id, + }, + } + } + fn transitive(eff: &Transition, cond: &Transition) -> Self { + Self { + supporter: TemplateEffID { + template_id: eff.template_id, + effect_id: eff.eff_id, + }, + condition: TemplateCondID { + template_id: cond.template_id, + cond_id: cond.cond_id, + }, + } + } +} + +struct Transition { + template_id: usize, + cond_id: usize, + eff_id: usize, + pre: GAtom, + post: GAtom, +} + +struct ConditionTemplate { + template_id: usize, + cond_id: usize, + pre: GAtom, +} + +fn find_conditions(fluent: &Fluent, pb: &Problem) -> Vec { + let mut conditions = Vec::new(); + for (template_id, ch) in pb.templates.iter().enumerate() { + let ch = &ch.chronicle; + for (cond_id, c) in ch.conditions.iter().enumerate() { + if c.state_var.fluent.as_ref() != fluent { + continue; + } + let pre = value_of(&c.state_var.fluent, &c.state_var.args, c.value); + conditions.push(ConditionTemplate { + template_id, + cond_id, + pre, + }) + } + } + conditions +} + +fn find_transitions(fluent: &Fluent, pb: &Problem) -> Vec { + let mut transitions = Vec::new(); + for (template_id, ch) in pb.templates.iter().enumerate() { + let ch = &ch.chronicle; + for (eff_id, e) in ch.effects.iter().enumerate() { + if e.state_var.fluent.as_ref() != fluent { + continue; + } + for (cond_id, c) in ch.conditions.iter().enumerate() { + let pre = value_of(&c.state_var.fluent, &c.state_var.args, c.value); + + if c.state_var == e.state_var && c.end == e.transition_start { + let EffectOp::Assign(val) = e.operation else { panic!() }; + let post = value_of(&e.state_var.fluent, &e.state_var.args, val); + transitions.push(Transition { + template_id, + cond_id, + eff_id, + pre, + post, + }); + break; + } + } + debug_assert!( + { + let t = transitions.last().unwrap(); + t.template_id == template_id && t.eff_id == eff_id + }, + "THe effect did not receive any matching condition (thus it is not a transition)" + ); + } + } + transitions +} + +fn gather_detrimental_supports( + fluent: &Fluent, + pb: &Problem, + useful_values: &HashSet, + detrimentals: &mut HashSet, +) { + let mut external_contributors = HashSet::new(); + for ch in &pb.templates { + let mut conds = HashMap::new(); + for c in &ch.chronicle.conditions { + if c.state_var.fluent.as_ref() == fluent { + conds.insert( + (&c.state_var, c.end), + value_of(&c.state_var.fluent, &c.state_var.args, c.value), + ); + } + } + for e in &ch.chronicle.effects { + if e.state_var.fluent.as_ref() == fluent { + let key = (&e.state_var, e.transition_start); + if !conds.contains_key(&key) { + return; // the fluent is not continuous + } else { + // conds.remove(&key).unwrap(); + } + } else { + let EffectOp::Assign(value) = e.operation else { panic!() }; + let atom = value_of(&e.state_var.fluent, &e.state_var.args, value); + if useful_values.contains(&atom) { + external_contributors.extend(conds.values().cloned()); + } + } + } + } + let single_useful_value = if external_contributors.len() != 1 { + None + } else { + let v = external_contributors.iter().next().unwrap(); + match v.value { + Value::SymCst(_) | Value::BoolCst(_) => Some(v.clone()), + _ => None, // may have different instantiations + } + }; + + println!(" - {}", &fluent.name); + for c in &external_contributors { + println!(" - {}", c.format(pb)); + } + + let conditions = find_conditions(fluent, pb); + let transitions = find_transitions(fluent, pb); + let supporters = |val: &GAtom| transitions.iter().filter(move |t| &t.post == val).collect_vec(); + let supported = |val: &GAtom| transitions.iter().filter(move |t| &t.pre == val).collect_vec(); + + if let Some(useful) = single_useful_value { + // we have a single useful value, any support for a transition that moves away is detrimental + // a transition that establishes the useful value must not be used only to enable a transition away from it + println!("single useful: {}", useful.format(pb)); + + for eff in &transitions { + if eff.pre == useful { + for cond in supported(&eff.post) { + detrimentals.insert(CausalSupport::transitive(eff, cond)); + } + } + } + + if transitions.iter().all(|t| t.pre == useful && t.post == useful) { + for c in conditions.iter().filter(|c| c.pre == useful) { + for eff in supporters(&c.pre) { + detrimentals.insert(CausalSupport { + supporter: TemplateEffID { + template_id: eff.template_id, + effect_id: eff.eff_id, + }, + condition: TemplateCondID { + template_id: c.template_id, + cond_id: c.cond_id, + }, + }); + } + } + } + } else { + // look for all transitions from useful values + let from_useful = transitions + .iter() + .filter(|t| external_contributors.contains(&t.pre)) + .collect_vec(); + let post_useful: HashSet<_> = from_useful.iter().map(|t| &t.post).collect(); + if post_useful.len() == 1 { + let transition_value = post_useful.iter().next().unwrap(); + if !external_contributors.contains(&transition_value) && transition_value.value.is_constant() { + // we have a single transition value from useful ones + // all this transitions are deemed detrimentals + // i.e. transitioning from a useful value to this catchall value is detrimental + for t1 in supported(transition_value) { + for t2 in supporters(transition_value) { + if t1.post == t2.pre { + // this is a transition from `transition_value -> useful_value -> transition_value` + detrimentals.insert(CausalSupport::transitive(t1, t2)); + } + } + } + } + } + } +} diff --git a/planning/planning/src/chronicles/analysis/hierarchy.rs b/planning/planning/src/chronicles/analysis/hierarchy.rs new file mode 100644 index 00000000..fcf2e80f --- /dev/null +++ b/planning/planning/src/chronicles/analysis/hierarchy.rs @@ -0,0 +1,110 @@ +use crate::chronicles::analysis::ProblemClass; +use crate::chronicles::{Chronicle, Problem}; +use aries::model::extensions::AssignmentExt; +use aries::model::lang::Atom; + +pub fn class_of(pb: &Problem) -> ProblemClass { + // TODO: this is an approximation as a hierarchical problem where no subtasks appears would be considered flat + if is_hierarchical(pb) { + if hierarchical_is_non_recursive(pb) { + ProblemClass::HierarchicalNonRecursive + } else { + ProblemClass::HierarchicalRecursive + } + } else if pb.templates.is_empty() { + ProblemClass::FlatNoTemplates + } else { + ProblemClass::FlatTemplates + } +} + +pub fn is_hierarchical(pb: &Problem) -> bool { + let has_subtask = |ch: &Chronicle| !ch.subtasks.is_empty(); + + pb.chronicles + .iter() + .map(|instance| &instance.chronicle) + .any(has_subtask) + || pb + .templates + .iter() + .map(|templates| &templates.chronicle) + .any(has_subtask) +} + +/// Returns true if the problem provably contains no cycles in the hierarchy. +pub fn hierarchical_is_non_recursive(pb: &Problem) -> bool { + let model = &pb.context.model; + + // roots of the graphs are all subtasks in concrete chronicles + let roots = pb + .chronicles + .iter() + .filter(|ch| !model.entails(!ch.chronicle.presence)) + .flat_map(|ch| ch.chronicle.subtasks.iter()) + .map(|subtask| subtask.task_name.as_slice()); + + // two task are considered equivalent for the purpose of cycle detection if they are unifiable + let equiv = |a: &[Atom], b: &[Atom]| model.unifiable_seq(a, b); + + is_acyclic( + roots, + // successors of a task are all subtasks of a template chronicle that can refine the tasl. + |task: &[Atom]| { + pb.templates + .iter() + .filter(move |tl| tl.chronicle.task.iter().any(|t| equiv(task, t))) + .flat_map(|tl| tl.chronicle.subtasks.iter()) + .map(|st| st.task_name.as_slice()) + }, + equiv, + ) +} + +/// Returns true if the graph contains a cycle. +/// +/// # Parameters +/// +/// - `roots`: entry points to the graph +/// - `succs`: function that assoicates each node with a list of its children +/// - `equiv`: function to test whether a given node was already +fn is_acyclic>( + roots: impl IntoIterator, + succs: impl Fn(T) -> Ts, + equiv: impl Fn(T, T) -> bool, +) -> bool { + // stack of the depth first search. + // Each node is labeled with its depth to allow maintaining the path from the root + let mut stack = Vec::with_capacity(32); + for x in roots.into_iter() { + stack.push((x, 0)); + } + + // history of traversed from the root to the current one + let mut path: Vec = Vec::with_capacity(32); + + // traverse the graph depth first until we exhaust it or en + while let Some((top, parent_depth)) = stack.pop() { + path.truncate(parent_depth); + if path.iter().any(|prev| equiv(*prev, top)) { + return false; + } + for succ in succs(top) { + stack.push((succ, parent_depth + 1)); + } + path.push(top); + } + + true +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_acyclic() { + assert!(is_acyclic(vec![0, 1], |i| (i + 1)..5, |x, y| x == y)); + assert!(!is_acyclic(vec![0, 1], |i| [(i + 1) % 5], |x, y| x == y)); + } +} diff --git a/planning/planning/src/chronicles/analysis/mod.rs b/planning/planning/src/chronicles/analysis/mod.rs index 13d3a734..0ea27f2a 100644 --- a/planning/planning/src/chronicles/analysis/mod.rs +++ b/planning/planning/src/chronicles/analysis/mod.rs @@ -1,80 +1,30 @@ use crate::chronicles::Problem; -use aries::model::extensions::AssignmentExt; -use aries::model::lang::Atom; +use std::collections::HashSet; -/// Returns true if the problem provably contains no cycles in the hierarchy. -pub fn hierarchical_is_non_recursive(pb: &Problem) -> bool { - let model = &pb.context.model; +mod detrimental_supports; +pub mod hierarchy; +mod static_fluents; - // roots of the graphs are all subtasks in concrete chronicles - let roots = pb - .chronicles - .iter() - .filter(|ch| !model.entails(!ch.chronicle.presence)) - .flat_map(|ch| ch.chronicle.subtasks.iter()) - .map(|subtask| subtask.task_name.as_slice()); +pub use detrimental_supports::{CausalSupport, TemplateCondID, TemplateEffID}; +pub use static_fluents::is_static; - // two task are considered equivalent for the purpose of cycle detection if they are unifiable - let equiv = |a: &[Atom], b: &[Atom]| model.unifiable_seq(a, b); - - is_acyclic( - roots, - // successors of a task are all subtasks of a template chronicle that can refine the tasl. - |task: &[Atom]| { - pb.templates - .iter() - .filter(move |tl| tl.chronicle.task.iter().any(|t| equiv(task, t))) - .flat_map(|tl| tl.chronicle.subtasks.iter()) - .map(|st| st.task_name.as_slice()) - }, - equiv, - ) +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub enum ProblemClass { + FlatNoTemplates, + FlatTemplates, + HierarchicalRecursive, + HierarchicalNonRecursive, } -/// Returns true if the graph contains a cycle. -/// -/// # Parameters -/// -/// - `roots`: entry points to the graph -/// - `succs`: function that assoicates each node with a list of its children -/// - `equiv`: function to test whether a given node was already -fn is_acyclic>( - roots: impl IntoIterator, - succs: impl Fn(T) -> Ts, - equiv: impl Fn(T, T) -> bool, -) -> bool { - // stack of the depth first search. - // Each node is labeled with its depth to allow maintaining the path from the root - let mut stack = Vec::with_capacity(32); - for x in roots.into_iter() { - stack.push((x, 0)); - } - - // history of traversed from the root to the current one - let mut path: Vec = Vec::with_capacity(32); - - // traverse the graph depth first until we exhaust it or en - while let Some((top, parent_depth)) = stack.pop() { - path.truncate(parent_depth); - if path.iter().any(|prev| equiv(*prev, top)) { - return false; - } - for succ in succs(top) { - stack.push((succ, parent_depth + 1)); - } - path.push(top); - } - - true +/// A set of metadata of a problem, typically gather through the analysis of the unbounded problem +pub struct Metadata { + pub class: ProblemClass, + pub detrimental_supports: HashSet, } -#[cfg(test)] -mod tests { - use crate::chronicles::analysis::is_acyclic; - - #[test] - fn test_acyclic() { - assert!(is_acyclic(vec![0, 1], |i| (i + 1)..5, |x, y| x == y)); - assert!(!is_acyclic(vec![0, 1], |i| [(i + 1) % 5], |x, y| x == y)); +pub fn analyse(pb: &Problem) -> Metadata { + Metadata { + class: hierarchy::class_of(pb), + detrimental_supports: detrimental_supports::find_useless_supports(pb), } } diff --git a/planning/planning/src/chronicles/analysis/static_fluents.rs b/planning/planning/src/chronicles/analysis/static_fluents.rs new file mode 100644 index 00000000..fa7a86ff --- /dev/null +++ b/planning/planning/src/chronicles/analysis/static_fluents.rs @@ -0,0 +1,74 @@ +use crate::chronicles::{concrete, EffectOp, Fluent, Problem, StateVar}; +use aries::model::extensions::AssignmentExt; +fn is_on_fluent(target_fluent: &Fluent, state_var: &StateVar) -> bool { + target_fluent == state_var.fluent.as_ref() +} + +/// Returns true if the fluent is static: all effects are at the temporal origin with no variables in it. +pub fn is_static(target_fluent: &Fluent, pb: &Problem) -> bool { + let context = &pb.context; + // convenience functions + let is_on_target_fluent = |state_var: &StateVar| is_on_fluent(target_fluent, state_var); + let effect_is_static_assignment = |eff: &concrete::Effect| -> bool { + // this effect is unifiable with our state variable, we can only make it static if all variables are bound + if eff + .state_var + .args + .iter() + .any(|y| context.model.sym_domain_of(*y).size() != 1) + { + return false; + } + // effect must be an assignment of a constant + if let EffectOp::Assign(value) = eff.operation { + let (lb, ub) = context.model.int_bounds(value); + if lb != ub { + return false; + } + } else { + return false; + } + eff.effective_start() == context.origin() + }; + // sf is the state function that we are evaluating for replacement. + // - first check that we are in fact allowed to replace it (it only has static effects and all conditions are convertible) + // - then transforms it: build a table with all effects and replace the conditions with table constraints + let mut template_effects = pb.templates.iter().flat_map(|ch| &ch.chronicle.effects); + + let appears_in_template_effects = template_effects.any(|eff| is_on_target_fluent(&eff.state_var)); + if appears_in_template_effects { + return false; // not a static state function (appears in template) + } + + let mut effects = pb.chronicles.iter().flat_map(|ch| ch.chronicle.effects.iter()); + + let effects_init_and_bound = effects.all(|eff| { + if is_on_target_fluent(&eff.state_var) { + // this effect is unifiable with our state variable, we can only make it static if all variables are bound + effect_is_static_assignment(eff) + } else { + true // not interesting, continue + } + }); + if !effects_init_and_bound { + return false; // not a static state function (appears after INIT or not full defined) + } + + // check that all conditions for this state variable can be converted to a table entry + let chronicles = pb + .templates + .iter() + .map(|tempplate| &tempplate.chronicle) + .chain(pb.chronicles.iter().map(|ch| &ch.chronicle)); + let mut conditions = chronicles.flat_map(|ch| ch.conditions.iter()); + let conditions_ok = conditions.all(|cond| { + if is_on_target_fluent(&cond.state_var) { + // the value of this condition must be transformable to an int + cond.value.int_view().is_some() + } else { + true // not interesting, continue + } + }); + + conditions_ok +} diff --git a/planning/planning/src/chronicles/mod.rs b/planning/planning/src/chronicles/mod.rs index 9231577f..494a6475 100644 --- a/planning/planning/src/chronicles/mod.rs +++ b/planning/planning/src/chronicles/mod.rs @@ -325,4 +325,5 @@ pub struct FiniteProblem { /// Timepoint after which no action is allowed pub makespan_ub: Time, pub chronicles: Vec, + pub meta: Arc, } diff --git a/planning/planning/src/chronicles/preprocessing/state_variables.rs b/planning/planning/src/chronicles/preprocessing/state_variables.rs index 018066f1..2e8bf388 100644 --- a/planning/planning/src/chronicles/preprocessing/state_variables.rs +++ b/planning/planning/src/chronicles/preprocessing/state_variables.rs @@ -1,5 +1,5 @@ +use crate::chronicles::analysis::is_static; use crate::chronicles::constraints::Constraint; -use crate::chronicles::preprocessing::statics::is_static; use crate::chronicles::{Chronicle, Container, Effect, EffectOp, Fluent, Problem, StateVar, Time, VarType}; use aries::model::lang::*; use aries::model::symbols::{SymId, TypedSym}; diff --git a/planning/planning/src/chronicles/preprocessing/statics.rs b/planning/planning/src/chronicles/preprocessing/statics.rs index 46406698..016f1df5 100644 --- a/planning/planning/src/chronicles/preprocessing/statics.rs +++ b/planning/planning/src/chronicles/preprocessing/statics.rs @@ -1,81 +1,14 @@ use crate::chronicles::*; +use crate::chronicles::analysis::is_static; use crate::chronicles::constraints::{Constraint, ConstraintType}; -use aries::model::extensions::{AssignmentExt, Shaped}; +use aries::model::extensions::Shaped; use std::convert::TryFrom; fn is_on_fluent(target_fluent: &Fluent, state_var: &StateVar) -> bool { target_fluent == state_var.fluent.as_ref() } -pub fn is_static(target_fluent: &Fluent, pb: &Problem) -> bool { - let context = &pb.context; - // convenience functions - let is_on_target_fluent = |state_var: &StateVar| is_on_fluent(target_fluent, state_var); - let effect_is_static_assignment = |eff: &concrete::Effect| -> bool { - // this effect is unifiable with our state variable, we can only make it static if all variables are bound - if eff - .state_var - .args - .iter() - .any(|y| context.model.sym_domain_of(*y).size() != 1) - { - return false; - } - // effect must be an assignment of a constant - if let EffectOp::Assign(value) = eff.operation { - let (lb, ub) = context.model.int_bounds(value); - if lb != ub { - return false; - } - } else { - return false; - } - eff.effective_start() == context.origin() - }; - // sf is the state function that we are evaluating for replacement. - // - first check that we are in fact allowed to replace it (it only has static effects and all conditions are convertible) - // - then transforms it: build a table with all effects and replace the conditions with table constraints - let mut template_effects = pb.templates.iter().flat_map(|ch| &ch.chronicle.effects); - - let appears_in_template_effects = template_effects.any(|eff| is_on_target_fluent(&eff.state_var)); - if appears_in_template_effects { - return false; // not a static state function (appears in template) - } - - let mut effects = pb.chronicles.iter().flat_map(|ch| ch.chronicle.effects.iter()); - - let effects_init_and_bound = effects.all(|eff| { - if is_on_target_fluent(&eff.state_var) { - // this effect is unifiable with our state variable, we can only make it static if all variables are bound - effect_is_static_assignment(eff) - } else { - true // not interesting, continue - } - }); - if !effects_init_and_bound { - return false; // not a static state function (appears after INIT or not full defined) - } - - // check that all conditions for this state variable can be converted to a table entry - let chronicles = pb - .templates - .iter() - .map(|tempplate| &tempplate.chronicle) - .chain(pb.chronicles.iter().map(|ch| &ch.chronicle)); - let mut conditions = chronicles.flat_map(|ch| ch.conditions.iter()); - let conditions_ok = conditions.all(|cond| { - if is_on_target_fluent(&cond.state_var) { - // the value of this condition must be transformable to an int - cond.value.int_view().is_some() - } else { - true // not interesting, continue - } - }); - - conditions_ok -} - /// Detects state functions that are static (all of its state variable will take a single value over the entire planning window) /// and replaces the corresponding conditions and effects as table constraints. ///