diff --git a/reasoner/src/main/java/eu/knowledge/engine/reasoner/rulenode/RuleNode.java b/reasoner/src/main/java/eu/knowledge/engine/reasoner/rulenode/RuleNode.java index 5d34df420..2089a8935 100644 --- a/reasoner/src/main/java/eu/knowledge/engine/reasoner/rulenode/RuleNode.java +++ b/reasoner/src/main/java/eu/knowledge/engine/reasoner/rulenode/RuleNode.java @@ -81,7 +81,7 @@ public Map> findAntecedentCoverage(Map> entry : someAntecedentNeighbors.entrySet()) { for (Match m : entry.getValue()) { - if (m.getMatchingPatterns().keySet().contains(tp)) { + if (m.getMatchingPatterns().containsValue(tp)) { coveringNodes.add(entry.getKey()); break; // where does this break from? The inner loop. } diff --git a/smart-connector/src/main/java/eu/knowledge/engine/smartconnector/impl/ReasonerProcessor.java b/smart-connector/src/main/java/eu/knowledge/engine/smartconnector/impl/ReasonerProcessor.java index 5640c5e5c..8b7fc579d 100644 --- a/smart-connector/src/main/java/eu/knowledge/engine/smartconnector/impl/ReasonerProcessor.java +++ b/smart-connector/src/main/java/eu/knowledge/engine/smartconnector/impl/ReasonerProcessor.java @@ -2,15 +2,13 @@ import java.io.IOException; import java.time.Instant; -import java.util.Collections; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import eu.knowledge.engine.reasoner.api.TripleNode; import org.apache.jena.graph.Node; import org.apache.jena.sparql.core.TriplePath; import org.apache.jena.sparql.core.Var; @@ -668,21 +666,21 @@ public Set getKnowledgeGaps(RuleNode plan) { assert plan instanceof AntSide; - Set existingOrGaps = new HashSet(); + Set existingOrGaps = new HashSet<>(); // TODO do we need to include the parent if we are not backward chaining? Map> nodeCoverage = plan .findAntecedentCoverage(((AntSide) plan).getAntecedentNeighbours()); // collect triple patterns that have an empty set - Set collectedOrGaps, someGaps = new HashSet(); + Set collectedOrGaps, someGaps = new HashSet<>(); for (Entry> entry : nodeCoverage.entrySet()) { LOG.debug("Entry key is {}", entry.getKey()); LOG.debug("Entry value is {}", entry.getValue()); - collectedOrGaps = new HashSet(); + collectedOrGaps = new HashSet<>(); boolean foundNeighborWithoutGap = false; for (RuleNode neighbor : entry.getValue()) { LOG.debug("Neighbor is {}", neighbor); @@ -711,40 +709,110 @@ public Set getKnowledgeGaps(RuleNode plan) { } LOG.debug("Found a neighbor without gaps is {}", foundNeighborWithoutGap); - if (!foundNeighborWithoutGap) { - // there is a gap here, either in the current node or in a neighbor. + if (foundNeighborWithoutGap) continue; - if (collectedOrGaps.isEmpty()) { - KnowledgeGap kg = new KnowledgeGap(); - kg.add(entry.getKey()); - collectedOrGaps.add(kg); - } - LOG.debug("CollectedOrGaps is {}", collectedOrGaps); - - Set newExistingOrGaps = new HashSet(); - if (existingOrGaps.isEmpty()) { - existingOrGaps.addAll(collectedOrGaps); - LOG.debug("Added collectedOrGaps to existingOrGaps"); - } else { - KnowledgeGap newGap; - for (KnowledgeGap existingOrGap : existingOrGaps) { - for (KnowledgeGap collectedOrGap : collectedOrGaps) { - newGap = new KnowledgeGap(); - newGap.addAll(existingOrGap); - newGap.addAll(collectedOrGap); - LOG.debug("Found newGap {}", newGap); - newExistingOrGaps.add(newGap); - } - } - existingOrGaps = newExistingOrGaps; - } + // there is a gap here, either in the current node or in a neighbor + if (collectedOrGaps.isEmpty()) { + KnowledgeGap kg = new KnowledgeGap(); + kg.add(entry.getKey()); + collectedOrGaps.add(kg); } + LOG.debug("CollectedOrGaps is {}", collectedOrGaps); + + existingOrGaps = mergeGaps(existingOrGaps, collectedOrGaps); } LOG.debug("Found existingOrGaps {}", existingOrGaps); return existingOrGaps; } + /** + * Given two triples, decides how they should be combined into a (single) knowledge gap + * + * @param existingTriple First triple + * @param newTriple Second triple + * @return Action that should be taken when merging these two triples in a knowledge gap + */ + private static TripleMatchType getTripleMatchType(TriplePattern existingTriple, TriplePattern newTriple) { + Map matches = existingTriple.findMatches(newTriple); + if (matches == null) { + return TripleMatchType.ADD_TRIPLE; + } + + ArrayList matchType = new ArrayList<>(); + for (Entry match : matches.entrySet()) { + if (match.getKey().node.isVariable() && match.getValue().node.isConcrete()) { + matchType.add(TripleMatchType.REPLACE_TRIPLE); + } else if (match.getKey().node.isConcrete() && match.getValue().node.isVariable()) { + matchType.add(TripleMatchType.IGNORE_TRIPLE); + } + } + + TripleMatchType result; + if (matchType.isEmpty()) { + result = TripleMatchType.IGNORE_TRIPLE; + } else if (matchType.stream().allMatch(m -> m.equals(matchType.get(0)))) { + result = matchType.get(0); + } else { + result = TripleMatchType.ADD_TRIPLE; + } + return result; + } + + /** + * Merges two Knowledge Gaps into one Knowledge Gap + * @param gap First knowledge gap + * @param gapToAdd Knowledge gap that should be added/merged into the first knowledge gap + * @return one Knowledge Gap containing triples from both provided knowledge gaps, ignoring triples that + * are duplicates or more generic (e.g. {@code ?id hasCountry ?c} is ignored if there is already + * {@code ?id hasCountry Ireland}) + */ + private static KnowledgeGap mergeGap(KnowledgeGap gap, KnowledgeGap gapToAdd) { + KnowledgeGap result = new KnowledgeGap(gap); + for (TriplePattern tripleToAdd : gapToAdd) { + Map tripleActions = new HashMap<>(); + for (TriplePattern existingTriple : gap) { + TripleMatchType tripleAction = getTripleMatchType(existingTriple, tripleToAdd); + tripleActions.put(existingTriple, tripleAction); + } + Set replaces = tripleActions.entrySet().stream().filter(t -> + t.getValue() == TripleMatchType.REPLACE_TRIPLE).map(Map.Entry::getKey).collect(Collectors.toSet()); + + if (replaces.size() == 1) { + TriplePattern toReplace = replaces.iterator().next(); + result.remove(toReplace); + result.add(tripleToAdd); + } else if (!tripleActions.containsValue(TripleMatchType.IGNORE_TRIPLE)) { + result.add(tripleToAdd); + } + } + return result; + } + + /** + * Given multiple knowledge gaps, adds the gaps in gapstoAdd to all knowledge gaps in listOfGaps + * @param listOfGaps list of knowledge gaps to which new knowledge gaps should be added + * @param gapsToAdd knowledge gaps that should be added to the gaps in listOfGaps + * @return Set of KnowledgeGaps where knowledge gaps in gapsToAdd have been merged/added to the gaps in listOfGaps + */ + private static Set mergeGaps(Set listOfGaps, Set gapsToAdd) { + if (listOfGaps.isEmpty()) { + return gapsToAdd; + } else if (gapsToAdd.isEmpty()) { + return listOfGaps; + } + + Set knowledgeGaps = new HashSet<>(); + for (KnowledgeGap existingGap : listOfGaps) { + for (KnowledgeGap gapToAdd : gapsToAdd) { + KnowledgeGap g = mergeGap(existingGap, gapToAdd); + knowledgeGaps.add(g); + } + } + + return knowledgeGaps; + } + private boolean isMetaKI(RuleNode neighbor) { assert neighbor.getRule() instanceof Rule; @@ -765,4 +833,13 @@ private boolean isMetaKI(RuleNode neighbor) { public void setMatchStrategy(MatchStrategy aStrategy) { this.matchStrategy = aStrategy; } + + /** + * Enum that represents which action to take for a triple when merging two knowledge gaps + * Given a triple, compared to another triple, the first triple can either be + * (1) Added to the Knowledge Gap, (2) Ignored, or (3) Replace the other triple + */ + private enum TripleMatchType { + ADD_TRIPLE, IGNORE_TRIPLE, REPLACE_TRIPLE + } } diff --git a/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestKnowledgeGapDetection.java b/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestKnowledgeGapDetection.java new file mode 100644 index 000000000..fd15e3b0c --- /dev/null +++ b/smart-connector/src/test/java/eu/knowledge/engine/smartconnector/api/TestKnowledgeGapDetection.java @@ -0,0 +1,199 @@ +package eu.knowledge.engine.smartconnector.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.BrokenBarrierException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +import org.apache.jena.shared.PrefixMapping; +import org.apache.jena.sparql.graph.PrefixMappingMem; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import eu.knowledge.engine.reasoner.ReasonerPlan; +import eu.knowledge.engine.reasoner.Rule; +import eu.knowledge.engine.reasoner.api.TriplePattern; +import eu.knowledge.engine.smartconnector.util.KnowledgeNetwork; +import eu.knowledge.engine.smartconnector.util.KnowledgeBaseImpl; + +public class TestKnowledgeGapDetection { + + private static final Logger LOG = LoggerFactory.getLogger(TestKnowledgeGapDetection.class); + + private KnowledgeBaseImpl kbEggObserver; + private KnowledgeBaseImpl kbImperialEggSearcher; + private KnowledgeNetwork kn; + private PrefixMappingMem prefixes; + private AskKnowledgeInteraction askKI; + private Set ruleSet; + + @BeforeEach + public void setup() throws InterruptedException, BrokenBarrierException, TimeoutException { + prefixes = new PrefixMappingMem(); + prefixes.setNsPrefixes(PrefixMapping.Standard); + prefixes.setNsPrefix("ex", "https://www.tno.nl/example/"); + addDomainKnowledge(); + + instantiateImperialEggSearcherKB(); + instantiateObserverKB(); + + kn = new KnowledgeNetwork(); + kn.addKB(kbEggObserver); + kn.addKB(kbImperialEggSearcher); + kn.sync(); + } + + private void addDomainKnowledge() { + this.ruleSet = new HashSet<>(); + HashSet consequent1 = new HashSet<>(); + consequent1.add(new TriplePattern( + "?id ")); + HashSet antecedent1 = new HashSet<>(); + antecedent1.add(new TriplePattern( + "?id ")); + antecedent1.add(new TriplePattern("?id \"Alexander III\"")); + antecedent1.add(new TriplePattern("?id \"Russia\"")); + antecedent1.add(new TriplePattern("?id \"House of Fabergé\"")); + this.ruleSet.add(new Rule("Domain knowledge", antecedent1, consequent1, new Rule.AntecedentToConsequentBindingSetHandler(antecedent1))); + } + + @Test + public void testKnowledgeGap() throws InterruptedException, ExecutionException { + AskPlan plan = kbImperialEggSearcher.planAsk(askKI, new RecipientSelector()); + ReasonerPlan rn = plan.getReasonerPlan(); + rn.getStore().printGraphVizCode(rn); + AskResult result = plan.execute(new BindingSet()).get(); + Set gaps = result.getKnowledgeGaps(); + + LOG.info("Found gaps: " + gaps); + + assertEquals(1, gaps.size()); + + Set expectedGap = new HashSet<>(); + expectedGap.add(new TriplePattern(prefixes, "?id ex:commissionedBy \"Alexander III\"")); + expectedGap.add(new TriplePattern(prefixes, "?id ex:madeIn \"Russia\"")); + expectedGap.add(new TriplePattern(prefixes, "?id ex:madeBy \"House of Fabergé\"")); + + assertEquals(expectedGap, gaps.toArray()[0]); + } + + + @Test + public void testKnowledgeGapNoMatchingVars() throws InterruptedException, ExecutionException { + GraphPattern gp = new GraphPattern(prefixes, + "?iq rdf:type . ?iq ?company . ?iq ?country . ?iq ?image ."); + this.askKI = new AskKnowledgeInteraction(new CommunicativeAct(), gp, "askImperialEggsNonMatching", false, false, true, MatchStrategy.SUPREME_LEVEL); + kbImperialEggSearcher.register(this.askKI); + kn.sync(); + + AskPlan plan = kbImperialEggSearcher.planAsk(askKI, new RecipientSelector()); + ReasonerPlan rn = plan.getReasonerPlan(); + rn.getStore().printGraphVizCode(rn); + + AskResult result = plan.execute(new BindingSet()).get(); + + Set gaps = result.getKnowledgeGaps(); + LOG.info("Found gaps: " + gaps); + + assertEquals(1, gaps.size()); + + Set expectedGap = new HashSet<>(); + expectedGap.add(new TriplePattern(prefixes, "?id ex:madeBy \"House of Fabergé\"")); + expectedGap.add(new TriplePattern(prefixes, "?id ex:madeIn \"Russia\"")); + expectedGap.add(new TriplePattern(prefixes, "?id ex:commissionedBy \"Alexander III\"")); + + assertEquals(new KnowledgeGap(expectedGap), gaps.toArray()[0]); + } + + @Test + public void testNoKnowledgeGap() throws InterruptedException, ExecutionException { + GraphPattern gp = new GraphPattern(prefixes, + "?iq rdf:type . ?iq ?image ."); + this.askKI = new AskKnowledgeInteraction(new CommunicativeAct(), gp, "askImperialEggNoGap", false, false, true, MatchStrategy.SUPREME_LEVEL); + kbImperialEggSearcher.register(this.askKI); + kn.sync(); + + AskPlan plan = kbImperialEggSearcher.planAsk(askKI, new RecipientSelector()); + ReasonerPlan rn = plan.getReasonerPlan(); + rn.getStore().printGraphVizCode(rn); + + AskResult result = plan.execute(new BindingSet()).get(); + + Set gaps = result.getKnowledgeGaps(); + LOG.info("Found gaps: " + gaps); + + assertEquals(0, gaps.size()); + } + + @Test + public void testKnowledgeGapWithoutPrefixes() throws InterruptedException, ExecutionException { + GraphPattern gp = new GraphPattern("?id . ?id ?image ."); + this.askKI = new AskKnowledgeInteraction(new CommunicativeAct(), gp, "askImperialEggNoGap", false, false, true, MatchStrategy.SUPREME_LEVEL); + kbImperialEggSearcher.register(this.askKI); + kn.sync(); + + AskPlan plan = kbImperialEggSearcher.planAsk(askKI, new RecipientSelector()); + ReasonerPlan rn = plan.getReasonerPlan(); + rn.getStore().printGraphVizCode(rn); + + AskResult result = plan.execute(new BindingSet()).get(); + Set gaps = result.getKnowledgeGaps(); + LOG.info("Found gaps: " + gaps); + + assertEquals(1, gaps.size()); + Set expectedGap = new HashSet<>(); + expectedGap.add(new TriplePattern(prefixes, "?id ex:commissionedBy \"Alexander III\"")); + expectedGap.add(new TriplePattern(prefixes, "?id ex:madeIn \"Russia\"")); + expectedGap.add(new TriplePattern(prefixes, "?id ex:madeBy \"House of Fabergé\"")); + assertEquals(expectedGap, gaps.toArray()[0]); + } + + public void instantiateImperialEggSearcherKB() { + kbImperialEggSearcher = new KnowledgeBaseImpl("ImperialEggSearcher"); + kbImperialEggSearcher.setReasonerEnabled(true); + + GraphPattern gp2 = new GraphPattern(prefixes, + "?id rdf:type . ?id ?company . ?id ?country . ?id ?image ."); + this.askKI = new AskKnowledgeInteraction(new CommunicativeAct(), gp2, "askImperialEggs", false, false, true, MatchStrategy.SUPREME_LEVEL); + kbImperialEggSearcher.register(this.askKI); + kbImperialEggSearcher.setDomainKnowledge(this.ruleSet); + } + + public void instantiateObserverKB() { + kbEggObserver = new KnowledgeBaseImpl("EggObserver"); + kbEggObserver.setReasonerEnabled(true); + + GraphPattern gp1 = new GraphPattern(prefixes, "?id . ?id ?image ."); + AnswerKnowledgeInteraction aKI = new AnswerKnowledgeInteraction(new CommunicativeAct(), gp1, "answerEggs"); + kbEggObserver.register(aKI, (AnswerHandler) (anAKI, anAnswerExchangeInfo) -> { + assertTrue( + anAnswerExchangeInfo.getIncomingBindings().isEmpty() + || anAnswerExchangeInfo.getIncomingBindings().iterator().next().getVariables().isEmpty(), + "Should not have bindings in this binding set."); + BindingSet bindingSet = new BindingSet(); + Binding binding1 = new Binding(); + binding1.put("id", ""); + binding1.put("image", "\"Picture Of Hen Fabergé Egg\"^^"); + bindingSet.add(binding1); + Binding binding2 = new Binding(); + binding2.put("id", ""); + binding2.put("image", "\"Picture of Third Imperial Fabergé Egg\""); + bindingSet.add(binding2); + + return bindingSet; + }); + } + + @AfterEach + public void cleanup() throws InterruptedException, ExecutionException { + LOG.info("Clean up: {}", TestKnowledgeGapDetection.class.getSimpleName()); + kn.stop().get(); + } +} \ No newline at end of file