Skip to content

Commit

Permalink
[fud2] Enumerative Planner (#2199)
Browse files Browse the repository at this point in the history
Implements a planner algorithm which uses a recursive search to
enumerate all possible plans. It then selects the first "valid" plan it
sees.

This serves as an alternative to the current path finding algorithm
which does not handle multi input/output ops.
  • Loading branch information
jku20 authored Jul 19, 2024
1 parent f320a21 commit d198a95
Show file tree
Hide file tree
Showing 2 changed files with 274 additions and 5 deletions.
150 changes: 145 additions & 5 deletions fud2/fud-core/src/exec/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,160 @@ pub trait FindPlan: std::fmt::Debug {
#[derive(Debug, Default)]
pub struct EnumeratePlanner {}
impl EnumeratePlanner {
/// The max number of ops in a searched for plan.
const MAX_PLAN_LEN: u32 = 7;

pub fn new() -> Self {
EnumeratePlanner {}
}

/// Returns `true` if executing `plan` will take `start` to `end`, going through all ops in `through`.
///
/// This function assumes all required inputs to `plan` exist or will be generated by `plan`.
fn valid_plan(plan: &[Step], end: &[StateRef], through: &[OpRef]) -> bool {
// Check all states in `end` are created.
let end_created = end
.iter()
.all(|s| plan.iter().any(|(_, states)| states.contains(s)));

// FIXME: Currently this checks that an outputs of an op specified by though is used.
// However, it's possible that the only use of this output by another op whose outputs
// are all unused. This means the plan doesn't actually use the specified op. but this
// code reports it would.
let through_used = through.iter().all(|t| {
plan.iter()
.any(|(op, used_states)| op == t && !used_states.is_empty())
});

end_created && through_used
}

/// A recursive function to generate all sequences prefixed by `plan` and containing `len` more
/// `Steps`. Returns a sequence such that applying `valid_plan` to the sequence results in `true.
/// If no such sequence exists, then `None` is returned.
///
/// `start` is the base inputs which can be used for ops.
/// `end` is the states to be generated by the return sequence of ops.
/// `ops` contains all usable operations to construct `Step`s from.
fn try_paths_of_length(
plan: &mut Vec<Step>,
len: u32,
start: &[StateRef],
end: &[StateRef],
through: &[OpRef],
ops: &PrimaryMap<OpRef, Operation>,
) -> Option<Vec<Step>> {
// Base case of the recursion. As `len == 0`, the algorithm reduces to applying `good` to
// `plan.
if len == 0 {
return if Self::valid_plan(plan, end, through) {
Some(plan.clone())
} else {
None
};
}

// Try adding every op to the back of the current `plan`. Then recurse on the subproblem.
for op_ref in ops.keys() {
// Check previous ops in the plan to see if any generated an input to `op_ref`.
let all_generated = ops[op_ref].input.iter().all(|input| {
// Check the outputs of ops earlier in the plan can be used as inputs to `op_ref`.
// `plan` is reversed so the latest versions of states are used.
plan.iter_mut().rev().any(|(o, _used_outputs)|
ops[*o].output.contains(input)
)
// As well as being generated in `plan`, `input` could be given in `start`.
|| start.contains(input)
});

// If this op cannot be uesd in the `plan` try a different one.
if !all_generated {
continue;
}

// Mark all used outputs.
let used_outputs_idxs: Vec<_> = ops[op_ref]
.input
.iter()
.filter_map(|input| {
// Get indicies of `Step`s whose `used_outputs` must be modified.
plan.iter_mut()
.rev()
.position(|(o, used_outputs)| {
// `op_ref`'s op now uses the input of the previous op in the plan.
// This should be noted in `used_outputs`.
!used_outputs.contains(input)
&& ops[*o].output.contains(input)
})
.map(|i| (input, i))
})
.collect();

for &(&input, i) in &used_outputs_idxs {
plan[i].1.push(input);
}

// Mark all outputs in `end` as used because they are used (or at least requested) by
// `end`.
let outputs = ops[op_ref].output.clone().into_iter();
let used_outputs =
outputs.filter(|s| end.contains(s)).collect::<Vec<_>>();

// Recurse! Now that `len` has been reduced by one, see if this new problem has a
// solution.
plan.push((op_ref, used_outputs));
if let Some(plan) = Self::try_paths_of_length(
plan,
len - 1,
start,
end,
through,
ops,
) {
return Some(plan);
}

// The investigated plan didn't work.
// Pop off the attempted element.
plan.pop();

// Revert modifications to `used_outputs`.
for &(_, i) in &used_outputs_idxs {
plan[i].1.pop();
}
}

// No sequence of `Step`s found :(.
None
}

/// Returns a sequence of `Step`s to transform `start` to `end`. The `Step`s are guaranteed to
/// contain all ops in `through`. If no such sequence exists, `None` is returned.
///
/// `ops` is a complete list of operations.
fn find_plan(
_start: &[StateRef],
_end: &[StateRef],
_through: &[OpRef],
_ops: &PrimaryMap<OpRef, Operation>,
start: &[StateRef],
end: &[StateRef],
through: &[OpRef],
ops: &PrimaryMap<OpRef, Operation>,
) -> Option<Vec<Step>> {
todo!()
// Try all sequences of ops up to `MAX_PATH_LEN`. At that point, the computation starts to
// become really big.
for len in 1..Self::MAX_PLAN_LEN {
if let Some(plan) = Self::try_paths_of_length(
&mut vec![],
len,
start,
end,
through,
ops,
) {
return Some(plan);
}
}

// No sequence of `Step`s found :(.
None
}
}

Expand Down
129 changes: 129 additions & 0 deletions fud2/fud-core/tests/tests.rs
Original file line number Diff line number Diff line change
@@ -1 +1,130 @@
use fud_core::{
exec::{EnumeratePlanner, FindPlan},
DriverBuilder,
};

#[test]
fn find_plan_simple_graph_test() {
let path_finder = EnumeratePlanner {};
let mut bld = DriverBuilder::new("fud2");
let s1 = bld.state("s1", &[]);
let s2 = bld.state("s2", &[]);
let t1 = bld.op("t1", &[], s1, s2, |_, _, _| Ok(()));
let driver = bld.build();
assert_eq!(
Some(vec![(t1, vec![s2])]),
path_finder.find_plan(&[s1], &[s2], &[], &driver.ops)
);
assert_eq!(None, path_finder.find_plan(&[s1], &[s1], &[], &driver.ops));
}

#[test]
fn find_plan_multi_op_graph() {
let path_finder = EnumeratePlanner {};
let mut bld = DriverBuilder::new("fud2");
let s1 = bld.state("s1", &[]);
let s2 = bld.state("s2", &[]);
let s3 = bld.state("s3", &[]);
let t1 = bld.op("t1", &[], s1, s3, |_, _, _| Ok(()));
let _ = bld.op("t2", &[], s2, s3, |_, _, _| Ok(()));
let driver = bld.build();
assert_eq!(
Some(vec![(t1, vec![s3])]),
path_finder.find_plan(&[s1], &[s3], &[], &driver.ops)
);
}

#[test]
fn find_plan_multi_path_graph() {
let path_finder = EnumeratePlanner {};
let mut bld = DriverBuilder::new("fud2");
let s1 = bld.state("s1", &[]);
let s2 = bld.state("s2", &[]);
let s3 = bld.state("s3", &[]);
let s4 = bld.state("s4", &[]);
let s5 = bld.state("s5", &[]);
let s6 = bld.state("s6", &[]);
let s7 = bld.state("s7", &[]);
let t1 = bld.op("t1", &[], s1, s3, |_, _, _| Ok(()));
let t2 = bld.op("t2", &[], s2, s3, |_, _, _| Ok(()));
let _ = bld.op("t3", &[], s3, s4, |_, _, _| Ok(()));
let t4 = bld.op("t4", &[], s3, s5, |_, _, _| Ok(()));
let t5 = bld.op("t5", &[], s3, s5, |_, _, _| Ok(()));
let _ = bld.op("t6", &[], s6, s7, |_, _, _| Ok(()));
let driver = bld.build();
assert_eq!(
Some(vec![(t1, vec![s3]), (t4, vec![s5])]),
path_finder.find_plan(&[s1], &[s5], &[], &driver.ops)
);
assert_eq!(
Some(vec![(t1, vec![s3]), (t5, vec![s5])]),
path_finder.find_plan(&[s1], &[s5], &[t5], &driver.ops)
);
assert_eq!(None, path_finder.find_plan(&[s6], &[s5], &[], &driver.ops));
assert_eq!(
None,
path_finder.find_plan(&[s1], &[s5], &[t2], &driver.ops)
);
}

#[test]
fn find_plan_only_state_graph() {
let path_finder = EnumeratePlanner {};
let mut bld = DriverBuilder::new("fud2");
let s1 = bld.state("s1", &[]);
let driver = bld.build();
assert_eq!(None, path_finder.find_plan(&[s1], &[s1], &[], &driver.ops));
}

#[test]
fn find_plan_self_loop() {
let path_finder = EnumeratePlanner {};
let mut bld = DriverBuilder::new("fud2");
let s1 = bld.state("s1", &[]);
let t1 = bld.op("t1", &[], s1, s1, |_, _, _| Ok(()));
let driver = bld.build();
assert_eq!(
Some(vec![(t1, vec![s1])]),
path_finder.find_plan(&[s1], &[s1], &[t1], &driver.ops)
);
}

#[test]
fn find_plan_cycle_graph() {
let path_finder = EnumeratePlanner {};
let mut bld = DriverBuilder::new("fud2");
let s1 = bld.state("s1", &[]);
let s2 = bld.state("s2", &[]);
let t1 = bld.op("t1", &[], s1, s2, |_, _, _| Ok(()));
let t2 = bld.op("t2", &[], s2, s1, |_, _, _| Ok(()));
let driver = bld.build();
assert_eq!(
Some(vec![(t1, vec![s2]), (t2, vec![s1])]),
path_finder.find_plan(&[s1], &[s1], &[], &driver.ops)
);
assert_eq!(
Some(vec![(t1, vec![s2])]),
path_finder.find_plan(&[s1], &[s2], &[], &driver.ops)
);
assert_eq!(
Some(vec![(t2, vec![s1])]),
path_finder.find_plan(&[s2], &[s1], &[], &driver.ops)
);
}

#[test]
fn find_plan_nontrivial_cycle() {
let path_finder = EnumeratePlanner {};
let mut bld = DriverBuilder::new("fud2");
let s1 = bld.state("s1", &[]);
let s2 = bld.state("s2", &[]);
let s3 = bld.state("s3", &[]);
let _t1 = bld.op("t1", &[], s2, s2, |_, _, _| Ok(()));
let t2 = bld.op("t2", &[], s1, s2, |_, _, _| Ok(()));
let t3 = bld.op("t3", &[], s2, s3, |_, _, _| Ok(()));
let driver = bld.build();
assert_eq!(
Some(vec![(t2, vec![s2]), (t3, vec![s3])]),
path_finder.find_plan(&[s1], &[s3], &[], &driver.ops)
);
}

0 comments on commit d198a95

Please sign in to comment.