diff --git a/src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/BackwardStateSpaceSearchTests.cs b/src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/BackwardStateSpaceSearchTests.cs index bdbe557..147d925 100644 --- a/src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/BackwardStateSpaceSearchTests.cs +++ b/src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/BackwardStateSpaceSearchTests.cs @@ -3,13 +3,15 @@ using SCClassicalPlanning.ExampleDomains.FromAIaMA; using SCClassicalPlanning.Planning.StateSpaceSearch.Heuristics; using SCFirstOrderLogic; +using SCFirstOrderLogic.Inference; +using SCFirstOrderLogic.Inference.Resolution; using static SCClassicalPlanning.ExampleDomains.FromAIaMA.AirCargo; using static SCClassicalPlanning.ExampleDomains.FromAIaMA.BlocksWorld; using static SCClassicalPlanning.ExampleDomains.FromAIaMA.SpareTire; +using static SCFirstOrderLogic.SentenceCreation.OperableSentenceFactory; namespace SCClassicalPlanning.Planning.StateSpaceSearch { -#if false public static class BackwardStateSpaceSearchTests { public static Test AirCargoScenario => TestThat @@ -25,6 +27,7 @@ public static class BackwardStateSpaceSearchTests return new TestCase( Domain: AirCargo.Domain, + Invariants: Array.Empty(), InitialState: new( Cargo(cargo1) & Cargo(cargo2) @@ -55,6 +58,7 @@ public static class BackwardStateSpaceSearchTests return new TestCase( Domain: BlocksWorld.Domain, + Invariants: new Sentence[] { ForAll(A, B, If(On(A, B), !Clear(B))), }, InitialState: new( Block(blockA) & Equal(blockA, blockA) @@ -88,6 +92,7 @@ public static class BackwardStateSpaceSearchTests return new TestCase( Domain: BlocksWorld.Domain, + Invariants: new Sentence[] { ForAll(A, B, If(On(A, B), !Clear(B))), }, InitialState: new( Block(blockA) & Equal(blockA, blockA) @@ -124,6 +129,7 @@ public static class BackwardStateSpaceSearchTests { return new TestCase( Domain: SpareTire.Domain, + Invariants: Array.Empty(), InitialState: new( SpareTire.ImplicitState & IsAt(Flat, Axle) @@ -136,16 +142,25 @@ public static class BackwardStateSpaceSearchTests .And((_, tc, p) => tc.Goal.IsSatisfiedBy(p.ApplyTo(tc.InitialState)).Should().BeTrue()) .And((cxt, _, p) => cxt.WriteOutputLine(new PlanFormatter(SpareTire.Domain).Format(p))); - private record TestCase(Domain Domain, State InitialState, Goal Goal) + private record TestCase(Domain Domain, IEnumerable Invariants, State InitialState, Goal Goal) { public Plan Execute() { var problem = new Problem(Domain, InitialState, Goal); - var heuristic = new IgnorePreconditionsGreedySetCover(problem); + + var invariantKb = new SimpleResolutionKnowledgeBase( + new SimpleClauseStore(), + SimpleResolutionKnowledgeBase.Filters.None, + SimpleResolutionKnowledgeBase.PriorityComparisons.UnitPreference); + invariantKb.Tell(Invariants); + + var innerHeuristic = new IgnorePreconditionsGreedySetCover(problem).EstimateCost; + //var innerHeuristic = ElementDifferenceCount.EstimateCost; + + var heuristic = new GoalInvariantCheck(invariantKb, innerHeuristic); var planner = new BackwardStateSpaceSearch(heuristic.EstimateCost); return planner.CreatePlanAsync(problem).GetAwaiter().GetResult(); } } } -#endif } diff --git a/src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/Heuristics/InvariantCheckTests.cs b/src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/Heuristics/GoalInvariantCheckTests.cs similarity index 80% rename from src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/Heuristics/InvariantCheckTests.cs rename to src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/Heuristics/GoalInvariantCheckTests.cs index 93f4700..b6de667 100644 --- a/src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/Heuristics/InvariantCheckTests.cs +++ b/src/SCClassicalPlanning.Tests/Planning/StateSpaceSearch/Heuristics/GoalInvariantCheckTests.cs @@ -12,7 +12,7 @@ namespace SCClassicalPlanning.Planning.StateSpaceSearch { - public static class InvariantCheckTests + public static class GoalInvariantCheckTests { private static readonly Constant blockA = new(nameof(blockA)); private static readonly Constant blockB = new(nameof(blockB)); @@ -31,7 +31,7 @@ public static class InvariantCheckTests & Clear(blockB) & Clear(blockC)); - private record TestCase(IEnumerable Invariants, State State, OperableGoal Goal, float ExpectedCost); + private record TestCase(IEnumerable Invariants, OperableState State, OperableGoal Goal, float ExpectedCost); public static Test EstimateCostBehaviour => TestThat .GivenTestContext() @@ -40,36 +40,42 @@ private record TestCase(IEnumerable Invariants, State State, OperableG new TestCase( Invariants: new Sentence[] { Block(blockA), ForAll(A, B, If(On(A, B), !Clear(B))) }, State: BlocksWorldInitialState, - Goal: Block(Table), // Fine - invariants don't rule this out + Goal: Goal.Empty, // Fine ExpectedCost: 0), new TestCase( Invariants: new Sentence[] { Block(blockA), ForAll(A, B, If(On(A, B), !Clear(B))) }, State: BlocksWorldInitialState, - Goal: !Block(blockA), // Contradicts Block(blockA) + Goal: Block(Table), // Fine + ExpectedCost: 0), + + new TestCase( + Invariants: new Sentence[] { Block(blockA), ForAll(A, B, If(On(A, B), !Clear(B))) }, + State: BlocksWorldInitialState, + Goal: !Block(blockA), // Violates Block(blockA) ExpectedCost: float.PositiveInfinity), new TestCase( Invariants: new Sentence[] { Block(blockA), ForAll(A, B, If(On(A, B), !Clear(B))) }, State: BlocksWorldInitialState, - Goal: On(blockA, blockB) & Clear(blockB), // Nope - violates on/clear relationship + Goal: On(blockA, blockB) & Clear(blockB), // Violates on/clear relationship ExpectedCost: float.PositiveInfinity), new TestCase( Invariants: new Sentence[] { Block(blockA), ForAll(A, B, If(On(A, B), !Clear(B))) }, State: BlocksWorldInitialState, Goal: On(blockB, blockA) & Clear(blockB), // Fine - ExpectedCost: 0) + ExpectedCost: 0), }) .When((_, tc) => { var kb = new SimpleResolutionKnowledgeBase( new SimpleClauseStore(), SimpleResolutionKnowledgeBase.Filters.None, - SimpleResolutionKnowledgeBase.PriorityComparisons.UnitPreference); + SimpleResolutionKnowledgeBase.PriorityComparisons.None); // No point in unitpref, 'cos query is all unit clauses.. kb.Tell(tc.Invariants); - return new InvariantCheck(kb, (s, g) => 0).EstimateCost(tc.State, tc.Goal); + return new GoalInvariantCheck(kb, (s, g) => 0).EstimateCost(tc.State, tc.Goal); }) .ThenReturns() .And((_, tc, rv) => rv.Should().Be(tc.ExpectedCost)); diff --git a/src/SCClassicalPlanning/Planning/StateSpaceSearch/Heuristics/GoalInvariantCheck.cs b/src/SCClassicalPlanning/Planning/StateSpaceSearch/Heuristics/GoalInvariantCheck.cs new file mode 100644 index 0000000..99a9fcb --- /dev/null +++ b/src/SCClassicalPlanning/Planning/StateSpaceSearch/Heuristics/GoalInvariantCheck.cs @@ -0,0 +1,93 @@ +using SCClassicalPlanning.ProblemManipulation; +using SCFirstOrderLogic; +using SCFirstOrderLogic.Inference; +using SCFirstOrderLogic.SentenceManipulation; + +namespace SCClassicalPlanning.Planning.StateSpaceSearch.Heuristics +{ + /// + /// A decorator heuristic that checks whether the goal violates any known invariants + /// before invoking the inner heuristic. If any invariants are violated, returns . + /// Intended to be of use for early pruning of unreachable goals when backward searching. + /// + /// NB #1: This heuristic isn't driven by any particular source material, but given that it's a fairly + /// obvious idea, there could well be some terminology that I'm not using - I may rename/refactor it as and when. + /// + /// NB #2: Checking invariants obviously comes at a performance cost (though fact that goals consist only of unit + /// clauses likely mitigates this quite a lot - because it means that the negation of the query we ask our KB it + /// consists only of unit clauses). + /// The question is whether the benefit it provides outweighs the cost. I do wonder if we can somehow check + /// only the stuff that has changed. + /// + /// NB #3: Ultimately it should be possible to derive the invariants by examining the problem. + /// The simplest example of this is if a predicate doesn't appear in any effects. If this is true, the + /// the occurences of this predicate in the initial state must persist throughout the problem. + /// Might research / play with this idea at some point. + /// + public class GoalInvariantCheck + { + private readonly Func innerHeuristic; + private readonly IKnowledgeBase knowledgeBase; + + /// + /// Initializes a new instance of the . + /// + /// A knowledge base containing all of the invariants of the problem. + /// The inner heuristic to invoke if no invariants are violated by the goal. + public GoalInvariantCheck(IKnowledgeBase invariantsKnowledgeBase, Func innerHeuristic) + { + this.innerHeuristic = innerHeuristic; + this.knowledgeBase = invariantsKnowledgeBase; + } + + /// + /// Estimates the cost of getting from the given state to a state that satisfies the given goal. + /// + /// The state. + /// The goal. + /// if any invariants are violated by the goal. Otherwise, the cost estimated by the inner heuristic. + public float EstimateCost(State state, Goal goal) + { + // One would assume that the inner heuristic would return 0 if there are no elements + // in the goal - but its not our business to shortcut that + if (goal.Elements.Count > 0) + { + var variables = new HashSet(); + GoalVariableFinder.Instance.Visit(goal, variables); + + // Annoying performance hit - goals are essentially already in CNF, but our knowledge bases want to do the conversion themselves.. Meh, never mind. + // TODO: Perhaps a ToSentence in Goal? (and others..) + var goalSentence = goal.Elements.Skip(1).Aggregate(goal.Elements.First().ToSentence(), (c, e) => new Conjunction(c, e.ToSentence())); + + foreach (var variable in variables) + { + goalSentence = new ExistentialQuantification(variable, goalSentence); + } + + // Note the negation here. We're not asking if the invariants mean that the goal MUST + // be true (that will of course generally not be the case!), we're asking if the goal + // CANNOT be true - that is, if its NEGATION must be true. + if (knowledgeBase.Ask(new Negation(goalSentence))) + { + return float.PositiveInfinity; + } + } + + return innerHeuristic(state, goal); + } + + /// + /// Utility class to find instances within the elements of a , and add them to a given . + /// + private class GoalVariableFinder : RecursiveGoalVisitor> + { + /// + /// Gets a singleton instance of the class. + /// + public static GoalVariableFinder Instance { get; } = new(); + + /// + public override void Visit(VariableDeclaration variable, HashSet variables) => variables.Add(variable); + } + } +} diff --git a/src/SCClassicalPlanning/Planning/StateSpaceSearch/Heuristics/InvariantCheck.cs b/src/SCClassicalPlanning/Planning/StateSpaceSearch/Heuristics/InvariantCheck.cs deleted file mode 100644 index 8941623..0000000 --- a/src/SCClassicalPlanning/Planning/StateSpaceSearch/Heuristics/InvariantCheck.cs +++ /dev/null @@ -1,67 +0,0 @@ -using SCFirstOrderLogic; -using SCFirstOrderLogic.Inference; - -namespace SCClassicalPlanning.Planning.StateSpaceSearch.Heuristics -{ - /// - /// A decorator heuristic that checks whether the goal violates any known invariants - /// before invoking the inner heuristic. If any invariants are violated, returns . - /// Intended to be of use for early pruning of unreachable goals when backward searching. - /// - /// This heuristic isn't driven by any particular source material, but given that it's a fairly - /// obvious idea, I'm assuming the approach has a name - I'll update it to use standard terminology as and when. - /// - /// Note that ultimately it should be possible to derive the invariants by examining the problem. - /// The simplest example of this is if a predicate doesn't appear in any effects. If this is true, the - /// the occurences of this predicate in the initial state must persist throughout the problem. - /// Might research / play with this idea at some point. - /// - public class InvariantCheck - { - private readonly Func innerHeuristic; - private readonly IKnowledgeBase knowledgeBase; - - /// - /// Initializes a new instance of the . - /// - /// A knowledge base containing all of the invariants of the problem. - /// The inner heuristic to invoke if no invariants are violated by the goal. - public InvariantCheck(IKnowledgeBase invariantsKnowledgeBase, Func innerHeuristic) - { - this.innerHeuristic = innerHeuristic; - this.knowledgeBase = invariantsKnowledgeBase; - } - - /// - /// Estimates the cost of getting from the given state to a state that satisfies the given goal. - /// - /// The state. - /// The goal. - /// if any invariants are violated by the goal. Otherwise, the cost estimated by the inner heuristic. - public float EstimateCost(State state, Goal goal) - { - if (goal.Elements.Count == 0) - { - return 0; - } - - // Performance hit - goals are essentially already in CNF, but our knowledge bases want to do the conversion themselves.. Meh, never mind. - // TODO: Perhaps a ToSentence in Goal? (and others..) - var goalSentence = goal.Elements.Skip(1).Aggregate(goal.Elements.First().ToSentence(), (c, e) => new Conjunction(c, e.ToSentence())); - - // Note the negation here. We're not asking if the invariants mean that the goal MUST - // be true (that will of course generally not be the case!), we're asking if the goal - // CANNOT be true - that is, if its NEGATION must be true. - // TODO: issues with variables in the goal here - which in goals are effectively existentially - // quantified, but KBs will i think assume universal. Q.. i *do* wonder if taking the Skolem - // function approach for goal vars is a more justifiable approach? But the book makes a - // big deal of CP being functionless.. - if (knowledgeBase.Ask(new Negation(goalSentence))) - { - return float.PositiveInfinity; - } - - return innerHeuristic(state, goal); - } - } -}