From 52ad3caa18f3c712410579566449b6ec33be5915 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Wed, 6 May 2026 11:02:45 +0000 Subject: [PATCH 1/5] feat(graphs): add method to query ancestors in graph --- .../java/org/tweetyproject/graphs/Graph.java | 109 +++++++++------ .../org/tweetyproject/graphs/GraphTest.java | 129 ++++++++++++++++++ 2 files changed, 200 insertions(+), 38 deletions(-) create mode 100644 org-tweetyproject-graphs/src/test/java/org/tweetyproject/graphs/GraphTest.java diff --git a/org-tweetyproject-graphs/src/main/java/org/tweetyproject/graphs/Graph.java b/org-tweetyproject-graphs/src/main/java/org/tweetyproject/graphs/Graph.java index a707a56ee..7cb7c17a3 100644 --- a/org-tweetyproject-graphs/src/main/java/org/tweetyproject/graphs/Graph.java +++ b/org-tweetyproject-graphs/src/main/java/org/tweetyproject/graphs/Graph.java @@ -1,27 +1,26 @@ -/* - * This file is part of "TweetyProject", a collection of Java libraries for - * logical aspects of artificial intelligence and knowledge representation. - * - * TweetyProject is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License version 3 as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - * - * Copyright 2016 The TweetyProject Team - */ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2016 The TweetyProject Team + */ package org.tweetyproject.graphs; -import java.util.Collection; -import java.util.Iterator; - -import org.tweetyproject.math.matrix.Matrix; +import java.util.*; + +import org.tweetyproject.math.matrix.Matrix; /** @@ -68,12 +67,12 @@ public interface Graph extends GeneralGraph{ * @return the number of nodes in this graph. */ public int getNumberOfNodes(); - - /** - * Returns the number of edges in this graph. - * @return the number of edges in this graph. - */ - public int getNumberOfEdges(); + + /** + * Returns the number of edges in this graph. + * @return the number of edges in this graph. + */ + public int getNumberOfEdges(); /** * Returns "true" iff the two nodes are connected by a directed edge @@ -130,7 +129,41 @@ public interface Graph extends GeneralGraph{ * @return the set of parents of the given node. */ public Collection getParents(Node node); - + + /** + * Returns the ancestors (nodes connected via an undirected or directed path + * where the given node is the descendant) of the given node. + * @param node some node (must be in the graph). + * @return the ancestors of the given node. + */ + public default Collection getAncestors(Node node) { + return getAncestors(List.of(node)); + } + + /** + * Returns the union of ancestors (nodes connected via an undirected or directed path + * where the given node is the descendant) of the given nodes. + * @param nodes some nodes (must be in the graph). + * @return union of ancestors of the given node. + */ + public default Collection getAncestors(Collection nodes) { + var ancestors = new HashSet(); + var visited = new HashSet(); + + var stack = new ArrayDeque(nodes); + while (!stack.isEmpty()) { + Node current = stack.pop(); + for (T parent : this.getParents(current)) { + if (!visited.contains(parent)) { + ancestors.add(parent); + stack.push(parent); + visited.add(parent); + } + } + } + return ancestors; + } + /** * Checks whether there is a (directed) path from node1 to node2. * @param node1 some node. @@ -167,15 +200,15 @@ public interface Graph extends GeneralGraph{ * @return the complement graph of this graph. */ public Graph getComplementGraph(int selfloops); - - /** - * Returns the set of (simple) connected components of this graph. - * A set of nodes is connected, if there is some path (ignoring edge - * directions) from each node to each other. It is a connected component - * if it is connected and maximal wrt. set inclusion. - * @return the connected components of this graph. - */ - public Collection> getConnectedComponents(); + + /** + * Returns the set of (simple) connected components of this graph. + * A set of nodes is connected, if there is some path (ignoring edge + * directions) from each node to each other. It is a connected component + * if it is connected and maximal wrt. set inclusion. + * @return the connected components of this graph. + */ + public Collection> getConnectedComponents(); /** * Returns the strongly connected components of this graph. A set diff --git a/org-tweetyproject-graphs/src/test/java/org/tweetyproject/graphs/GraphTest.java b/org-tweetyproject-graphs/src/test/java/org/tweetyproject/graphs/GraphTest.java new file mode 100644 index 000000000..e669cc358 --- /dev/null +++ b/org-tweetyproject-graphs/src/test/java/org/tweetyproject/graphs/GraphTest.java @@ -0,0 +1,129 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2016 The TweetyProject Team + */ +package org.tweetyproject.graphs; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.List; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +public class GraphTest { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void getAncestorsOfNode() { + var graph = new SimpleGraph<>(); + var nodeA = new SimpleNode("a"); + var nodeB = new SimpleNode("b"); + var nodeC = new SimpleNode("c"); + graph.addAll(List.of(nodeA, nodeB, nodeC)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeA, nodeB), + new DirectedEdge<>(nodeB, nodeC)) + ); + + var ancestors = graph.getAncestors(nodeC); + + assertEquals(Set.of(nodeA, nodeB), ancestors); + } + + @Test + public void getAncestorsOfNodeWithCycle() { + var graph = new SimpleGraph<>(); + var nodeA = new SimpleNode("a"); + var nodeB = new SimpleNode("b"); + var nodeC = new SimpleNode("c"); + graph.addAll(List.of(nodeA, nodeB, nodeC)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeA, nodeB), + new DirectedEdge<>(nodeB, nodeC), + new DirectedEdge<>(nodeC, nodeA)) + ); + + var ancestors = graph.getAncestors(nodeC); + + assertEquals(Set.of(nodeA, nodeB, nodeC), ancestors); + } + + @Test + public void getAncestorsOfNodeThrowWithNodeNotInGraph() { + var graph = new SimpleGraph<>(); + SimpleNode node = new SimpleNode("aNode"); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("The node is not in this graph."); + graph.getAncestors(node); + } + + @Test + public void getAncestorsOfNodesInSeparateStronglyConnectedComponents() { + var graph = new SimpleGraph<>(); + var nodeA = new SimpleNode("a"); + var nodeB = new SimpleNode("b"); + var nodeC = new SimpleNode("c"); + graph.addAll(List.of(nodeA, nodeB, nodeC)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeA, nodeB), + new DirectedEdge<>(nodeB, nodeC)) + ); + var nodeL = new SimpleNode("l"); + var nodeM = new SimpleNode("m"); + var nodeN = new SimpleNode("n"); + graph.addAll(List.of(nodeL, nodeM, nodeN)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeL, nodeM), + new DirectedEdge<>(nodeM, nodeN)) + ); + + var ancestors = graph.getAncestors(List.of(nodeC, nodeN)); + + assertEquals(Set.of(nodeA, nodeB, nodeL, nodeM), ancestors); + } + + @Test + public void getAncestorsOfNodesInSameStronglyConnectedComponents() { + var graph = new SimpleGraph<>(); + var nodeA = new SimpleNode("a"); + var nodeB = new SimpleNode("b"); + var nodeC = new SimpleNode("c"); + var nodeD = new SimpleNode("d"); + var nodeE = new SimpleNode("e"); + var nodeF = new SimpleNode("f"); + graph.addAll(List.of(nodeA, nodeB, nodeC, nodeD, nodeE, nodeF)); + graph.addAllEdges(List.of( + new DirectedEdge<>(nodeA, nodeB), + new DirectedEdge<>(nodeB, nodeC), + new DirectedEdge<>(nodeB, nodeD), + new DirectedEdge<>(nodeB, nodeD), + new DirectedEdge<>(nodeD, nodeE), + new DirectedEdge<>(nodeE, nodeF) + )); + + var ancestors = graph.getAncestors(List.of(nodeB, nodeE)); + + assertEquals(3, ancestors.size()); + assertEquals(Set.of(nodeA, nodeB, nodeD), ancestors); + } +} \ No newline at end of file From 7ee49f25d2b9f178372fce9028e28690c47fca64 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Wed, 6 May 2026 11:14:40 +0000 Subject: [PATCH 2/5] feat(causal): implement CausalParser --- .../causal/parser/CausalParser.java | 65 ++++++++- .../causal/syntax/StructuralCausalModel.java | 2 +- .../causal/parser/CausalParserTest.java | 133 ++++++++++++++++++ .../org/tweetyproject/commons/Parser.java | 29 +++- 4 files changed, 223 insertions(+), 6 deletions(-) create mode 100644 org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/parser/CausalParserTest.java diff --git a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/parser/CausalParser.java b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/parser/CausalParser.java index 304e25c3e..c83a67fb8 100644 --- a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/parser/CausalParser.java +++ b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/parser/CausalParser.java @@ -19,24 +19,83 @@ package org.tweetyproject.causal.parser; import org.tweetyproject.causal.syntax.CausalKnowledgeBase; +import org.tweetyproject.causal.syntax.StructuralCausalModel; import org.tweetyproject.commons.Parser; import org.tweetyproject.commons.ParserException; +import org.tweetyproject.logics.pl.parser.PlParserFactory; +import org.tweetyproject.logics.pl.syntax.PlBeliefSet; import org.tweetyproject.logics.pl.syntax.PlFormula; +import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** + * Parser for {@link CausalKnowledgeBase} and observation as consumed by {@link org.tweetyproject.causal.reasoner.AbstractCausalReasoner}. + * A causal knowledge base can be parsed with {@link #parseBeliefBase(Reader)}. + * Observations can be parsed with {@link #parseListOfFormulae(String, String)}. * + * @author Lars Bengel + * @author Oleksandr Dzhychko */ public class CausalParser extends Parser { + static final Pattern ASSUMPTIONS_PATTERN = Pattern.compile("^\\s*\\{(.*)\\}\\s*$"); + static final String SYMBOL_COMMA = ","; + static Parser plParser = PlParserFactory.getParserForFormat(PlParserFactory.Format.TWEETY); + + /** + * Parses data from the reader into a {@link CausalKnowledgeBase}. + * Each line must contain either assumptions or are an equation. + * Assumptions and equations are defined as following: + *
equation ::= formula '<=>' formula + *
assumptions ::= '{' assumption (',' assumption)* '}' + *
assumption ::= formula + *
formula ::= a propositional formula as parsable by {@link org.tweetyproject.logics.pl.parser.PlParser#parseFormula(String)} + * + * @param reader a reader + * @return the parsed causal knowledge base + * @throws IOException if some IO issue occurred. + * @throws ParserException some parsing exceptions may be added here. + */ @Override public CausalKnowledgeBase parseBeliefBase(Reader reader) throws IOException, ParserException { - return null; + // Implementation similar to AbaParser.parseBeliefBase. + // But it simplified by not allowing empty lines or comments. + // If needed, it can be added later. + + List assumptions = new ArrayList<>(); + List equations = new ArrayList<>(); + BufferedReader br = new BufferedReader(reader); + while (true) { + String line = br.readLine(); + if (line == null) break; + Matcher matcher = ASSUMPTIONS_PATTERN.matcher(line); + if (matcher.matches()) { + String[] assumptionStrings = matcher.group(1).split(SYMBOL_COMMA); + for (String assumptionString : assumptionStrings) + if (!assumptionString.isBlank()) { + assumptions.add(parseFormula(assumptionString)); + } + } else { + equations.add(parseFormula(line)); + } + } + + StructuralCausalModel model; + try { + model = new StructuralCausalModel(equations); + } catch (IllegalArgumentException | StructuralCausalModel.CyclicDependencyException e) { + throw new ParserException(e); + } + return new CausalKnowledgeBase(model, assumptions); } @Override public PlFormula parseFormula(Reader reader) throws IOException, ParserException { - return null; + return plParser.parseFormula(reader); } -} +} \ No newline at end of file diff --git a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java index 594e7a0be..a14233fa2 100644 --- a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java +++ b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/syntax/StructuralCausalModel.java @@ -379,7 +379,7 @@ public void clear() { /** * Thrown to indicate that the structural equations of a causal model contain a cyclic dependency */ - public static class CyclicDependencyException extends Throwable { + public static class CyclicDependencyException extends Exception { /** * Constructs a CyclicDependencyException with the specified detail message * diff --git a/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/parser/CausalParserTest.java b/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/parser/CausalParserTest.java new file mode 100644 index 000000000..a9bc32218 --- /dev/null +++ b/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/parser/CausalParserTest.java @@ -0,0 +1,133 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.causal.parser; + +import org.junit.jupiter.api.Test; +import org.tweetyproject.commons.ParserException; +import org.tweetyproject.logics.pl.syntax.Equivalence; +import org.tweetyproject.logics.pl.syntax.Negation; +import org.tweetyproject.logics.pl.syntax.Proposition; +import org.tweetyproject.logics.pl.syntax.Tautology; + +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; +import static org.tweetyproject.causal.syntax.StructuralCausalModel.*; + +/** + * @author Oleksandr Dzhychko + */ +public class CausalParserTest { + + CausalParser parser = new CausalParser(); + + @Test + public void parseBeliefBase() throws IOException { + var a = new Proposition("a"); + var b = new Proposition("b"); + var c = new Proposition("c"); + var d = new Proposition("d"); + var input = """ + a <=> b + c <=> d + { d, !b } + """; + + var knowledgeBase = parser.parseBeliefBase(input); + + assertEquals(Set.of(new Equivalence(a, b), new Equivalence(c, d)), knowledgeBase.getBeliefs()); + assertEquals(Set.of(new Negation(b), d), knowledgeBase.getAssumptions()); + } + + @Test + public void parseBeliefBaseWithMultipleAssumptionLines() throws IOException { + var a = new Proposition("a"); + var b = new Proposition("b"); + var c = new Proposition("c"); + var d = new Proposition("d"); + // Being able to break up assumptions to different lines + // allows to freely structure the knowledgeable. + var input = """ + { !b } + a <=> b + c <=> d + { d } + """; + + var knowledgeBase = parser.parseBeliefBase(input); + + assertEquals(Set.of(new Equivalence(a, b), new Equivalence(c, d)), knowledgeBase.getBeliefs()); + assertEquals(Set.of(new Negation(b), d), knowledgeBase.getAssumptions()); + } + + @Test + public void parseBeliefBaseWithEmptyAssumptions() throws IOException { + var a = new Proposition("a"); + var input = """ + a <=> + + {} + """; + + var knowledgeBase = parser.parseBeliefBase(input); + + assertEquals(Set.of(new Equivalence(a, new Tautology())), knowledgeBase.getBeliefs()); + } + + @Test + public void throwsParserExceptionForInvalidSyntax() { + var input = """ + (a + """; + + assertThrows(ParserException.class, () -> parser.parseBeliefBase(input)); + } + + @Test + public void throwsParserExceptionForFormulaThatIsNotAnEquivalence() { + var input = """ + a + """; + + assertThrows(ParserException.class, () -> parser.parseBeliefBase(input)); + } + + @Test + public void throwsParserExceptionForCyclicDependency() { + var input = """ + a <=> a + """; + + var exception = assertThrows(ParserException.class, () -> parser.parseBeliefBase(input)); + assertInstanceOf(CyclicDependencyException.class, exception.getCause()); + } + + + @Test + public void parseObservations() throws IOException { + var a = new Proposition("a"); + var b = new Proposition("b"); + var input = "a, !b"; + + var observations = parser.parseListOfFormulae(input, ","); + + assertEquals(List.of(a, new Negation(b)), observations); + } +} \ No newline at end of file diff --git a/org-tweetyproject-commons/src/main/java/org/tweetyproject/commons/Parser.java b/org-tweetyproject-commons/src/main/java/org/tweetyproject/commons/Parser.java index 9cf16467c..edd2bfe47 100644 --- a/org-tweetyproject-commons/src/main/java/org/tweetyproject/commons/Parser.java +++ b/org-tweetyproject-commons/src/main/java/org/tweetyproject/commons/Parser.java @@ -151,8 +151,7 @@ public List parseListOfBeliefBases(String text) throws ParserException, IOExc * @throws ParserException some parsing exception */ public List parseListOfBeliefBases(String text, String delimiter) throws ParserException, IOException { - if (delimiter.matches(".*" + illegalDelimitors + ".*")) - throw new IllegalArgumentException("The given delimiter is similar to characters that are likely to appear in formulas. Try using a more unique delimiter."); + assertDelimiterIsLegal(delimiter); String[] kbs_string = text.split(delimiter); ArrayList kbs = new ArrayList(); for (String kb_string : kbs_string) { @@ -215,4 +214,30 @@ public static boolean isNumeric(String str) { return true; } + /** + * Parses the given text into a list of formulae of the given type. + * Formulae are separated by the given delimiter. + * + * @param text a string + * @param delimiter for separating formulae + * @return a list of formulae in the order in which they appear in the input + * string. + * @throws IOException if an IO error occurs + * @throws ParserException some parsing exception + */ + public List parseListOfFormulae(String text, String delimiter) throws IOException, ParserException { + assertDelimiterIsLegal(delimiter); + String[] formulaStrings = text.split(delimiter); + List formulae = new ArrayList<>(); + for (String formulaString : formulaStrings) { + if (!formulaString.isBlank()) + formulae.add(this.parseFormula(formulaString)); + } + return formulae; + } + + private void assertDelimiterIsLegal(String delimiter) { + if (delimiter.matches(".*" + illegalDelimitors + ".*")) + throw new IllegalArgumentException("The given delimiter is similar to characters that are likely to appear in formulas. Try using a more unique delimiter."); + } } From 7a97e2ec0bb7f15a1c851913b83e627b8fb3e40a Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Wed, 6 May 2026 11:17:43 +0000 Subject: [PATCH 3/5] feat(causal): extend reasoner to query significant atoms --- .../ArgumentationBasedCausalReasoner.java | 110 +++++++++++++++++- .../ArgumentationBasedCausalReasonerTest.java | 54 +++++++++ 2 files changed, 158 insertions(+), 6 deletions(-) diff --git a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java index 126c708a8..e41c280c4 100644 --- a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java +++ b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java @@ -28,7 +28,9 @@ import org.tweetyproject.commons.util.SetTools; import org.tweetyproject.logics.pl.syntax.*; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -44,12 +46,7 @@ public class ArgumentationBasedCausalReasoner extends AbstractArgumentationBasedCausalReasoner { @Override public DungTheory getInducedTheory(CausalKnowledgeBase cbase, Collection observations, Map interventions) { - StructuralCausalModel model = cbase.getCausalModel(); - for (Proposition atom : interventions.keySet()) { - model = model.intervene(atom, interventions.get(atom)); - } - PlBeliefSet base = new PlBeliefSet(model.getStructuralEquations()); - base.addAll(observations); + PlBeliefSet base = createBeliefSetWithObservationsAndInterventions(cbase, observations, interventions); Collection literals = new HashSet<>(); for (Proposition atom : base.getSignature()) { @@ -111,6 +108,19 @@ public DungTheory getInducedTheory(CausalKnowledgeBase cbase, Collection observations, + Map interventions) { + StructuralCausalModel model = cbase.getCausalModel(); + for (Proposition atom : interventions.keySet()) { + model = model.intervene(atom, interventions.get(atom)); + } + PlBeliefSet base = new PlBeliefSet(model.getStructuralEquations()); + base.addAll(observations); + return base; + } + /** * Determines whether the given causal statements holds under the causal knowledge base * @@ -132,4 +142,92 @@ public boolean query(CausalKnowledgeBase cbase, CausalStatement statement) { public boolean query(CausalKnowledgeBase cbase, InterventionalStatement statement) { return query(cbase, statement.getObservations(), statement.getInterventions(), statement.getConclusion()); } + + /** + * Computes, for each atom that appears in the knowledge base, the set of atoms that are + * significant for establishing that conclusion under the given observations and interventions. + *

+ * This method: + *

    + *
  • Induces an argumentation theory from the given knowledge base, observations, + * and interventions.
  • + *
  • Groups arguments by the (single) atom occurring in their conclusion (positive + * or negated).
  • + *
  • Collects, per atom, all arguments concluding that atom (or its negation) and + * all their ancestors in the attack graph.
  • + *
  • For each of these arguments, retrieves kernels for the argument’s conclusion + * under a belief set extended with the argument’s premises, and gathers all + * atoms that appear in those kernels.
  • + *
+ * + * @param cbase some causal knowledge base + * @param observations some logical formulae representing the observations of causal atoms + * @param interventions a set of interventions on causal atoms + * @param atomFilter atoms for which to get the significant atoms. + * If {@code null}, the filter is not applied. + * @return the argumentation framework induced from the causal knowledge base and the observations + */ + public Map> getSignificantAtoms( + CausalKnowledgeBase cbase, + Collection observations, + Map interventions, + Collection atomFilter) { + var theory = getInducedTheory(cbase, observations, interventions); + var perAtomArgumentsWithAtomInConclusion = getPerAtomArgumentsWithAtomInConclusion(theory, atomFilter); + var beliefSetWithoutAssumptions = createBeliefSetWithObservationsAndInterventions(cbase, observations, interventions); + + var perAtomSignificantAtoms = new HashMap>(); + + for (var entry : perAtomArgumentsWithAtomInConclusion.entrySet()) { + var atom = entry.getKey(); + var argumentsForAtom = entry.getValue(); + + var significantArguments = new HashSet<>(); + significantArguments.addAll(argumentsForAtom); + significantArguments.addAll(theory.getAncestors(argumentsForAtom)); + + var significantAtoms = new HashSet(); + for (var argument : significantArguments) { + var causalArgument = (CausalArgument) argument; + var beliefSetWithAssumptions = new PlBeliefSet(beliefSetWithoutAssumptions); + beliefSetWithAssumptions.addAll(causalArgument.getPremises()); + var kernels = reasoner.getKernels(beliefSetWithAssumptions, causalArgument.getConclusion()); + for (var kernel : kernels) { + for (var formula : kernel) { + significantAtoms.addAll(formula.getAtoms()); + } + } + } + perAtomSignificantAtoms.put(atom, significantAtoms); + } + + return perAtomSignificantAtoms; + } + + + /** + * Returns, for each atom, the set of arguments whose conclusion is the atom or its negation. + * + * @param theory the theory containing the arguments + * @param atomFilter atoms for which to get the significant atoms. + * If {@code null}, the filter is not applied. + * @return a map from atom to the set of matching arguments + */ + private Map> getPerAtomArgumentsWithAtomInConclusion(DungTheory theory, Collection atomFilter) { + var perAtomArguments = new HashMap>(); + + for (var argument: theory) { + var causalArgument = (CausalArgument) argument; + var signature = causalArgument.getConclusion().getAtoms(); + if (signature.size() != 1) { + throw new IllegalStateException("Encountered invalid argument with more than one atom in the its conclusion: " + causalArgument); + } + var atom = signature.stream().findFirst().get(); + if (atomFilter != null && !atomFilter.contains(atom)) continue; + + var arguments = perAtomArguments.computeIfAbsent(atom, (_atom) -> new ArrayList<>()); + arguments.add(causalArgument); + } + return perAtomArguments; + } } diff --git a/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java b/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java index 8c31ae5bf..f0b974114 100644 --- a/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java +++ b/org-tweetyproject-causal/src/test/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasonerTest.java @@ -18,10 +18,64 @@ */ package org.tweetyproject.causal.reasoner; +import org.junit.jupiter.api.Test; +import org.tweetyproject.causal.syntax.CausalKnowledgeBase; +import org.tweetyproject.causal.syntax.StructuralCausalModel; +import org.tweetyproject.logics.pl.syntax.Equivalence; +import org.tweetyproject.logics.pl.syntax.Negation; +import org.tweetyproject.logics.pl.syntax.PlFormula; +import org.tweetyproject.logics.pl.syntax.Proposition; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + public class ArgumentationBasedCausalReasonerTest extends AbstractCausalReasonerTestBase { @Override protected ArgumentationBasedCausalReasoner createReasoner() { return new ArgumentationBasedCausalReasoner(); } + + @Test + public void getSignificantAtoms() throws StructuralCausalModel.CyclicDependencyException { + Proposition corona = new Proposition("corona"); + Proposition influenza = new Proposition("influenza"); + Proposition atRisk = new Proposition("at-risk"); + Proposition covid = new Proposition("covid"); + Proposition flu = new Proposition("flu"); + Proposition shortOfBreath = new Proposition("short-of-breath"); + Proposition fever = new Proposition("fever"); + Proposition chills = new Proposition("chills"); + Collection modelEquations = List.of( + new Equivalence(covid, corona), + new Equivalence(flu, influenza), + new Equivalence(fever, covid.combineWithOr(flu)), + new Equivalence(chills, fever), + new Equivalence(shortOfBreath, covid.combineWithAnd(atRisk)) + ); + StructuralCausalModel model = new StructuralCausalModel(modelEquations); + List assumptions = List.of(atRisk, corona, new Negation(influenza)); + CausalKnowledgeBase knowledgeBase = new CausalKnowledgeBase(model, assumptions); + Collection observations = List.of(covid); + ArgumentationBasedCausalReasoner reasoner = createReasoner(); + + var perAtomInfluencingAtoms = reasoner.getSignificantAtoms(knowledgeBase, observations, Map.of(), null); + + Map> perConclusionExpectedInfluencingAtoms = Map.of( + atRisk, Set.of(atRisk), + corona, Set.of(corona, covid), + influenza, Set.of(influenza), + covid, Set.of(covid), + shortOfBreath, Set.of(atRisk, shortOfBreath, covid), + fever, Set.of(fever, flu, covid), + chills, Set.of(covid, fever, chills, flu), + flu, Set.of(influenza, flu) + ); + + assertEquals(perConclusionExpectedInfluencingAtoms, perAtomInfluencingAtoms); + } } \ No newline at end of file From 45cb4b8e6657b6670ad8d1372620406c8707c733 Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Wed, 6 May 2026 11:45:57 +0000 Subject: [PATCH 4/5] feat(web): implement /sequence-explanation endpoint --- org-tweetyproject-web/pom.xml | 10 +- .../web/services/RequestController.java | 93 +++++ .../ArgumentFilterSerialization.java | 45 +++ .../ArgumentSerialization.java | 47 +++ .../sequenceexplanation/AttackDTO.java | 62 ++++ .../DialectialSequenceExplanationDTO.java | 64 ++++ .../SequenceExplanationPost.java | 103 ++++++ .../SequenceExplanationResponse.java | 109 ++++++ .../SequenceExplanationService.java | 65 ++++ ...uestControllerSequenceExplanationTest.java | 323 ++++++++++++++++++ 10 files changed, 920 insertions(+), 1 deletion(-) create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentFilterSerialization.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentSerialization.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationPost.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationResponse.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationService.java create mode 100644 org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerSequenceExplanationTest.java diff --git a/org-tweetyproject-web/pom.xml b/org-tweetyproject-web/pom.xml index 6a24c5e2f..1e7ed1add 100644 --- a/org-tweetyproject-web/pom.xml +++ b/org-tweetyproject-web/pom.xml @@ -56,7 +56,10 @@ org.springframework.boot spring-boot-starter-web - + + org.springframework.boot + spring-boot-starter-validation + org.springframework.boot spring-boot-starter-test @@ -111,6 +114,11 @@ pl 1.30-SNAPSHOT + + org.tweetyproject.arg + explanations + 1.30-SNAPSHOT + org.tweetyproject.arg delp diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java index bdebb812c..84bcb5182 100644 --- a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java @@ -25,7 +25,9 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -33,8 +35,11 @@ import java.util.concurrent.TimeoutException; import org.json.JSONException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; +import org.tweetyproject.arg.dung.syntax.Argument; +import org.tweetyproject.arg.dung.syntax.Attack; import org.tweetyproject.arg.dung.syntax.DungTheory; import org.tweetyproject.commons.BeliefSet; import org.tweetyproject.commons.Formula; @@ -79,6 +84,11 @@ import org.tweetyproject.web.services.incmes.InconsistencyGetMeasuresResponse; import org.tweetyproject.web.services.incmes.InconsistencyPost; import org.tweetyproject.web.services.incmes.InconsistencyValueResponse; +import org.tweetyproject.web.services.sequenceexplanation.*; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationPost.GetSequenceExplanationsCmd; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationPost.SequenceExplanationCmd; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationResponse.GetSequenceExplanationsResult; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationResponse.SequenceExplanationResult; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RequestBody; import org.tweetyproject.arg.aba.parser.AbaParser; @@ -97,6 +107,8 @@ import org.tweetyproject.arg.dung.semantics.Extension; import javafx.util.Pair; + +import javax.validation.Valid; import java.util.logging.Level; @@ -109,7 +121,14 @@ public class RequestController { private final int SERVICES_TIMEOUT_DUNG = 600; private final int SERVICES_TIMEOUT_DELP = 600; private final int SERVICES_TIMEOUT_INCMES = 300; + private final int SERVICES_TIMEOUT_SEQUENCE_EXPLANATION = 300; + + private final SequenceExplanationService sequenceExplanationService; + @Autowired + public RequestController(SequenceExplanationService sequenceExplanationService) { + this.sequenceExplanationService = sequenceExplanationService; + } /** @@ -696,5 +715,79 @@ private AbaGetSemanticsResponse handleGetSemantics(AbaReasonerPost query) return response; } + @PostMapping(value = "/sequence-explanation", produces = "application/json") + @ResponseBody + public SequenceExplanationResponse handleRequest(@Valid @RequestBody SequenceExplanationPost request) { + LoggerUtil.logger.info(String.format("Run sequence explanation command \"%s\" for user \"%s\" with timeout: %s %s", + request.getCmd().getClass().getSimpleName(), + request.getEmail(), + request.getTimeout(), + request.getUnit_timeout())); + + TimeUnit timeoutUnit = Utils.getTimoutUnit(request.getUnit_timeout()); + int timeout = Utils.checkUserTimeout(request.getTimeout(), SERVICES_TIMEOUT_SEQUENCE_EXPLANATION, timeoutUnit); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Pair resultAndExecutionTime; + try { + var future = executor.submit(() -> processCommand(request.getCmd())); + resultAndExecutionTime = Utils.runServicesWithTimeout(future, timeout, timeoutUnit); + } catch (TimeoutException e) { + LoggerUtil.logger.info("Timeout while running sequence explanation."); + return new SequenceExplanationResponse( + null, + request.getEmail(), + timeout, + request.getUnit_timeout(), + SequenceExplanationResponse.Status.TIMEOUT + ); + } catch (ExecutionException e) { + LoggerUtil.logger.warning(() -> "Error while running sequence explanation reasoner: " + e.getMessage()); + e.printStackTrace(); + throw new RuntimeException(e); + } catch (InterruptedException e) { + LoggerUtil.logger.warning(() -> "Interrupt while running sequence explanation: " + e.getMessage()); + e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread was interrupted."); + } finally { + executor.shutdownNow(); + } + + long executionTime = resultAndExecutionTime.getValue(); + var result = resultAndExecutionTime.getKey(); + return new SequenceExplanationResponse( + result, + request.getEmail(), + executionTime, + request.getUnit_timeout(), + SequenceExplanationResponse.Status.SUCCESS + ); + } + + private SequenceExplanationResult processCommand(SequenceExplanationCmd cmd) { + if (cmd instanceof GetSequenceExplanationsCmd) { + return processSequenceExplanationCmd((GetSequenceExplanationsCmd) cmd); + } else { + throw new IllegalStateException("Encountered invalid command:" + cmd.getClass().getSimpleName()); + } + } + private GetSequenceExplanationsResult processSequenceExplanationCmd(GetSequenceExplanationsCmd cmd) { + var theory = new DungTheory(); + for (AttackDTO attackDTO: cmd.getAttacks()) { + var attacker = new Argument(attackDTO.getAttacker()); + var attacked = new Argument(attackDTO.getAttacked()); + theory.add(attacker); + theory.add(attacked); + var attack = new Attack(attacker, attacked); + theory.add(attack); + } + Set argumentFilter = ArgumentFilterSerialization.deserialize(cmd.getArgumentFilter()); + if (argumentFilter != null) { + theory.addAll(argumentFilter); + } + var sequenceExplanation = sequenceExplanationService.querySequenceExplanations(theory, argumentFilter); + return GetSequenceExplanationsResult.from(sequenceExplanation); + } } diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentFilterSerialization.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentFilterSerialization.java new file mode 100644 index 000000000..90017bfd8 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentFilterSerialization.java @@ -0,0 +1,45 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import org.springframework.lang.Nullable; +import org.tweetyproject.arg.dung.syntax.Argument; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public class ArgumentFilterSerialization { + + /** + * Deserialize the filter for arguments. + * + * @param argumentFilter List of {@link Argument} names or {@code null} + * @return Set of {@link Argument}s or {@code null} if the input is {@code null} or empty. + */ + public static @Nullable Set deserialize(@Nullable List argumentFilter) { + if (argumentFilter == null || argumentFilter.isEmpty()) { + return null; + } + return argumentFilter.stream().map(Argument::new).collect(Collectors.toUnmodifiableSet()); + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentSerialization.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentSerialization.java new file mode 100644 index 000000000..ac814916f --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentSerialization.java @@ -0,0 +1,47 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import org.tweetyproject.arg.dung.syntax.Argument; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + + +/** + * @author Oleksandr Dzhychko + */ +public final class ArgumentSerialization { + public static String from(Argument argument) { + return argument.toString(); + } + + public static List fromCollection(Collection arguments) { + return arguments.stream() + .map(ArgumentSerialization::from) + .collect(Collectors.toUnmodifiableList()); + } + + public static List> fromCollectionOfCollections(List> collectionsOfArguments) { + return collectionsOfArguments.stream() + .map(ArgumentSerialization::fromCollection) + .collect(Collectors.toUnmodifiableList()); + } +} \ No newline at end of file diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java new file mode 100644 index 000000000..a4114377c --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/AttackDTO.java @@ -0,0 +1,62 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.lang.NonNull; +import org.tweetyproject.arg.dung.syntax.Attack; + +import javax.validation.constraints.NotNull; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class AttackDTO { + private final @NonNull @NotNull String attacker; + private final @NonNull @NotNull String attacked; + + public AttackDTO( + @JsonProperty(value="attacker", required = true) @NonNull @NotNull String attacker, + @JsonProperty(value="attacked", required = true) @NonNull @NotNull String attacked) { + this.attacker = attacker; + this.attacked = attacked; + } + + public static List from(Collection attacks) { + return attacks.stream() + .map(AttackDTO::from) + .collect(Collectors.toUnmodifiableList()); + } + + public static AttackDTO from(Attack attack) { + return new AttackDTO(attack.getAttacker().toString(), attack.getAttacked().toString()); + } + + + public String getAttacker() { + return attacker; + } + + public String getAttacked() { + return attacked; + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java new file mode 100644 index 000000000..cb1451af9 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java @@ -0,0 +1,64 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class DialectialSequenceExplanationDTO { + private final String argument; + private final List> supporters; + private final List> defeated; + + public DialectialSequenceExplanationDTO(String argument, List> supporters, List> defeated) { + this.argument = argument; + this.supporters = supporters; + this.defeated = defeated; + } + + public String getArgument() { + return argument; + } + + public List> getSupporters() { + return supporters; + } + + public List> getDefeated() { + return defeated; + } + + public static DialectialSequenceExplanationDTO from(DialectialSequenceExplanation explanation) { + return new DialectialSequenceExplanationDTO( + explanation.getArgument().toString(), + ArgumentSerialization.fromCollectionOfCollections(explanation.getSupporters()), + ArgumentSerialization.fromCollectionOfCollections(explanation.getDefeated()) + ); + } + + public static List from(List explanations) { + return explanations.stream().map(DialectialSequenceExplanationDTO::from) + .collect(Collectors.toUnmodifiableList()); + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationPost.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationPost.java new file mode 100644 index 000000000..4a058588a --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationPost.java @@ -0,0 +1,103 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.lang.Nullable; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author Oleksandr Dzhychko + */ +public class SequenceExplanationPost { + + public SequenceExplanationPost( + @JsonProperty("email") + String email, + @JsonProperty(value = "timeout", required = true) + int timeout, + @JsonProperty(value = "unit_timeout", required = true) + String unit_timeout, + @JsonProperty(value = "cmd", required = true) + SequenceExplanationCmd cmd + ) { + this.email = email; + this.timeout = timeout; + this.unit_timeout = unit_timeout; + this.cmd = cmd; + } + + private final @Nullable String email; + private final int timeout; + private final @NotNull String unit_timeout; + private final @Valid @NotNull SequenceExplanationCmd cmd; + + public String getEmail() { + return email; + } + + public int getTimeout() { + return timeout; + } + + public String getUnit_timeout() { + return unit_timeout; + } + + public SequenceExplanationCmd getCmd() { + return cmd; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = GetSequenceExplanationsCmd.class, name = "get_sequence_explanations"), + }) + public interface SequenceExplanationCmd { + } + + public static class GetSequenceExplanationsCmd implements SequenceExplanationCmd { + /** The attack relations represented as a list of lists of integers.*/ + private final @Valid @NotNull List<@NotNull AttackDTO> attacks; + private final @Valid @Nullable List<@NotNull String> argumentFilter; + + public GetSequenceExplanationsCmd( + @JsonProperty(value="attacks", required = true) + List attacks, + @JsonProperty(value = "argument_filter") + List argumentFilter) { + this.attacks = attacks; + this.argumentFilter = argumentFilter; + } + + public List getAttacks() { + return attacks; + } + + @Nullable + public List getArgumentFilter() { + return argumentFilter; + } + } + +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationResponse.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationResponse.java new file mode 100644 index 000000000..c026b2200 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationResponse.java @@ -0,0 +1,109 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.springframework.lang.NonNull; +import org.tweetyproject.web.services.sequenceexplanation.SequenceExplanationService.SequenceExplanations; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Response to {@link SequenceExplanationPost} + * + * @author Oleksandr Dzhychko + */ +public final class SequenceExplanationResponse { + + public enum Status { + SUCCESS, + TIMEOUT, + } + + private final SequenceExplanationResult reply; + private final String email; + private final double time; + private final String unit_timeout; + private final Status status; + + public SequenceExplanationResponse(SequenceExplanationResult reply, String email, double time, @NonNull String unit_timeout, Status status) { + this.reply = reply; + this.email = email; + this.time = time; + this.status = status; + this.unit_timeout = unit_timeout; + } + + public SequenceExplanationResult getReply() { + return reply; + } + + public String getEmail() { + return email; + } + + public double getTime() { + return time; + } + + @NonNull + public String getUnit_timeout() { + return unit_timeout; + } + + public Status getStatus() { + return status; + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXTERNAL_PROPERTY, property = "type") + @JsonSubTypes({ + @JsonSubTypes.Type(value = GetSequenceExplanationsResult.class, name = "get_sequence_explanations"), + }) + public interface SequenceExplanationResult { + } + + public static class GetSequenceExplanationsResult implements SequenceExplanationResult { + private final Map> perArgumentSequenceExplanations; + + public GetSequenceExplanationsResult(Map> perArgumentSequenceExplanations) { + this.perArgumentSequenceExplanations = perArgumentSequenceExplanations; + } + + public Map> getPerArgumentSequenceExplanations() { + return perArgumentSequenceExplanations; + } + + public static GetSequenceExplanationsResult from(SequenceExplanations sequenceExplanations) { + + var perArgumentSequenceExplanations = new LinkedHashMap>(); + for (var entry: sequenceExplanations.getPerArgumentSequenceExplanations().entrySet()) { + var argument = entry.getKey(); + var forArgumentSequenceExplanations = entry.getValue(); + var forArgumentSequenceExplanationDTOs = DialectialSequenceExplanationDTO.from(forArgumentSequenceExplanations); + perArgumentSequenceExplanations.put(argument.getName(), forArgumentSequenceExplanationDTOs); + } + + return new GetSequenceExplanationsResult(perArgumentSequenceExplanations); + } + } + +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationService.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationService.java new file mode 100644 index 000000000..0aa6f994f --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/SequenceExplanationService.java @@ -0,0 +1,65 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.sequenceexplanation; + +import org.springframework.stereotype.Service; +import org.tweetyproject.arg.dung.syntax.Argument; +import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.arg.explanations.reasoner.acceptance.DialecticalSequenceExplanationReasoner; +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +@Service +public final class SequenceExplanationService { + private final DialecticalSequenceExplanationReasoner explanationReasoner = new DialecticalSequenceExplanationReasoner(); + + public SequenceExplanations querySequenceExplanations(DungTheory theory, Set argumentFilter) { + var perArgumentSequenceExplanations = new LinkedHashMap>(); + for (var argument: theory) { + if (argumentFilter != null && !argumentFilter.contains(argument)) { + continue; + } + var explanations = explanationReasoner.getExplanations(theory, argument); + var sequenceExplanations = explanations.stream() + .map(explanation -> (DialectialSequenceExplanation) explanation) + .collect(Collectors.toUnmodifiableList()); + var allSequenceExplanations = new ArrayList(); + perArgumentSequenceExplanations.put(argument, allSequenceExplanations); + allSequenceExplanations.addAll(sequenceExplanations); + } + return new SequenceExplanations(perArgumentSequenceExplanations); + } + + public static final class SequenceExplanations { + private final Map> perArgumentSequenceExplanations; + + public SequenceExplanations(Map> perAtomSequenceExplanations) { + this.perArgumentSequenceExplanations = perAtomSequenceExplanations; + } + + public Map> getPerArgumentSequenceExplanations() { + return perArgumentSequenceExplanations; + } + } +} diff --git a/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerSequenceExplanationTest.java b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerSequenceExplanationTest.java new file mode 100644 index 000000000..9b8c08630 --- /dev/null +++ b/org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerSequenceExplanationTest.java @@ -0,0 +1,323 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.stream.Stream; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +/** + * @author Oleksandr Dzhychko + */ +@SpringBootTest +@AutoConfigureMockMvc +class RequestControllerSequenceExplanationTest { + @Autowired + private MockMvc mvc; + + private static Stream badRequestsBodies() { + return Stream.of(Arguments.of("unit_timeout null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": null, + "cmd": { + "type": "get_sequence_explanations", + "attacks": [] + } + } + """), Arguments.of("cmd null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": null + } + """), Arguments.of("attacks null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": null + } + } + """), Arguments.of("attack null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [null] + } + } + """), Arguments.of("attacker null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [{ + "attacker": null, + "attacked": "a" + }] + } + } + """), Arguments.of("attacked null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [{ + "attacker": "a", + "attacked": null + }] + } + } + """), Arguments.of("argument null", + // language=JSON + """ + { + "email": null, + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [], + "argument_filter": [null] + } + } + """)); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("badRequestsBodies") + public void sequenceExplanationBadRequest(String name, String requestBody) throws Exception { + var post = post("/sequence-explanation").contentType(MediaType.APPLICATION_JSON).content(requestBody); + + mvc.perform(post).andExpect(status().isBadRequest()); + } + + @Test + public void sequenceExplanationsForAllArguments() throws Exception { + var post = post("/sequence-explanation").contentType(MediaType.APPLICATION_JSON) + // language=JSON + .content(""" + { + "email": "aId", + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [ + { + "attacker": "a", + "attacked": "b" + }, + { + "attacker": "b", + "attacked": "a" + } + ] + } + } + """); + + mvc.perform(post).andExpect(status().isOk()) + // language=JSON + .andExpect(content().json(""" + { + "reply": { + "type": "get_sequence_explanations", + "perArgumentSequenceExplanations": { + "a": [ + { + "argument": "a", + "supporters": [ + [ + "a" + ] + ], + "defeated": [ + [ + "b" + ] + ] + } + ], + "b": [ + { + "argument": "b", + "supporters": [ + [ + "b" + ] + ], + "defeated": [ + [ + "a" + ] + ] + } + ] + } + }, + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void sequenceExplanationsForSelectedArguments() throws Exception { + var post = post("/sequence-explanation").contentType(MediaType.APPLICATION_JSON) + // language=JSON + .content(""" + { + "email": "aId", + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [ + { + "attacker": "a", + "attacked": "b" + }, + { + "attacker": "b", + "attacked": "a" + } + ], + "argument_filter": ["b"] + } + } + """); + + mvc.perform(post).andExpect(status().isOk()) + // language=JSON + .andExpect(content().json(""" + { + "reply": { + "type": "get_sequence_explanations", + "perArgumentSequenceExplanations": { + "b": [ + { + "argument": "b", + "supporters": [ + [ + "b" + ] + ], + "defeated": [ + [ + "a" + ] + ] + } + ] + } + }, + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void sequenceExplanationsForSelectedArgumentThatIsNotInAttackes() throws Exception { + var post = post("/sequence-explanation").contentType(MediaType.APPLICATION_JSON) + // language=JSON + .content(""" + { + "email": "aId", + "timeout": 10, + "unit_timeout": "s", + "cmd": { + "type": "get_sequence_explanations", + "attacks": [], + "argument_filter": ["a"] + } + } + """); + + mvc.perform(post).andExpect(status().isOk()) + // language=JSON + .andExpect(content().json(""" + { + "reply": { + "type": "get_sequence_explanations", + "perArgumentSequenceExplanations": { + "a": [ + { + "argument": "a", + "supporters": [ + [ + "a" + ] + ], + "defeated": [ + [] + ] + } + ] + } + }, + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } +} \ No newline at end of file From f1522570e31d5925d6ccaf3d2f3f0417425eec9d Mon Sep 17 00:00:00 2001 From: Oleksandr Dzhychko Date: Wed, 6 May 2026 11:54:51 +0000 Subject: [PATCH 5/5] feat(web): implement /causal endpoint --- .../ArgumentationBasedCausalReasoner.java | 2 +- org-tweetyproject-web/pom.xml | 5 + .../web/services/RequestController.java | 176 +++++++++++++- .../causal/ArgumentationFrameworkReply.java | 56 +++++ .../services/causal/CausalReasonerPost.java | 184 +++++++++++++++ .../causal/CausalReasonerResponse.java | 86 +++++++ .../causal/CausalReasonerService.java | 111 +++++++++ .../ConclusionsFilterSerialization.java | 75 ++++++ .../causal/SequenceExplanationReply.java | 67 ++++++ .../DialectialSequenceExplanationDTO.java | 1 + .../ArgumentSerialization.java | 4 +- .../services/RequestControllerCausalTest.java | 222 ++++++++++++++++++ 12 files changed, 985 insertions(+), 4 deletions(-) create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentationFrameworkReply.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerResponse.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java create mode 100644 org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java rename org-tweetyproject-web/src/main/java/org/tweetyproject/web/{services/sequenceexplanation => util}/ArgumentSerialization.java (96%) create mode 100644 org-tweetyproject-web/src/test/java/org/tweetyproject/web/services/RequestControllerCausalTest.java diff --git a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java index e41c280c4..54d170e3c 100644 --- a/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java +++ b/org-tweetyproject-causal/src/main/java/org/tweetyproject/causal/reasoner/ArgumentationBasedCausalReasoner.java @@ -213,7 +213,7 @@ public Map> getSignificantAtoms( * If {@code null}, the filter is not applied. * @return a map from atom to the set of matching arguments */ - private Map> getPerAtomArgumentsWithAtomInConclusion(DungTheory theory, Collection atomFilter) { + public Map> getPerAtomArgumentsWithAtomInConclusion(DungTheory theory, Collection atomFilter) { var perAtomArguments = new HashMap>(); for (var argument: theory) { diff --git a/org-tweetyproject-web/pom.xml b/org-tweetyproject-web/pom.xml index 1e7ed1add..3a4d7e771 100644 --- a/org-tweetyproject-web/pom.xml +++ b/org-tweetyproject-web/pom.xml @@ -104,6 +104,11 @@ 4.13.1 test + + org.tweetyproject + causal + 1.30-SNAPSHOT + org.tweetyproject.logics commons diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java index 84bcb5182..ad48e3845 100644 --- a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/RequestController.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -34,13 +35,20 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.json.JSONException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; import org.tweetyproject.arg.dung.syntax.Argument; import org.tweetyproject.arg.dung.syntax.Attack; import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.causal.parser.CausalParser; +import org.tweetyproject.causal.syntax.CausalKnowledgeBase; import org.tweetyproject.commons.BeliefSet; import org.tweetyproject.commons.Formula; import org.tweetyproject.commons.Parser; @@ -71,6 +79,7 @@ import org.tweetyproject.web.services.aba.AbaReasonerPost; import org.tweetyproject.web.services.aba.AbaReasonerResponse; import org.tweetyproject.web.services.aba.GeneralAbaReasonerFactory; +import org.tweetyproject.web.services.causal.*; import org.tweetyproject.web.services.delp.DeLPCallee; import org.tweetyproject.web.services.delp.DeLPPost; import org.tweetyproject.web.services.delp.DeLPResponse; @@ -111,6 +120,9 @@ import javax.validation.Valid; import java.util.logging.Level; +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.SUCCESS; +import static org.tweetyproject.web.services.causal.CausalReasonerResponse.Status.TIMEOUT; + /** * andles HTTP POST requests at the provided endpoints @@ -122,12 +134,19 @@ public class RequestController { private final int SERVICES_TIMEOUT_DELP = 600; private final int SERVICES_TIMEOUT_INCMES = 300; private final int SERVICES_TIMEOUT_SEQUENCE_EXPLANATION = 300; + private final int SERVICES_TIMEOUT_CAUSAL = 300; private final SequenceExplanationService sequenceExplanationService; + private final ObjectMapper objectMapper; + private final CausalReasonerService causalReasonerService; @Autowired - public RequestController(SequenceExplanationService sequenceExplanationService) { + public RequestController(SequenceExplanationService sequenceExplanationService, + ObjectMapper objectMapper, + CausalReasonerService causalReasonerService) { this.sequenceExplanationService = sequenceExplanationService; + this.objectMapper = objectMapper; + this.causalReasonerService = causalReasonerService; } @@ -790,4 +809,159 @@ private GetSequenceExplanationsResult processSequenceExplanationCmd(GetSequenceE var sequenceExplanation = sequenceExplanationService.querySequenceExplanations(theory, argumentFilter); return GetSequenceExplanationsResult.from(sequenceExplanation); } + + /** + * Executes the causal reasoner as specified by the provided {@link CausalReasonerPost} + * + * @param request The request payload containing information for causal reasoning + * @return A Response object containing the result of the ABA reasoning operation. + */ + @PostMapping(value = "/causal", produces = "application/json") + @ResponseBody + public CausalReasonerResponse handleRequest(@Valid @RequestBody CausalReasonerPost request) { + LoggerUtil.logger.info(String.format("Run causal reasoner command \"%s\" for user \"%s\" with timeout: %s %s", + request.getCmd(), + request.getEmail(), + request.getTimeout(), + request.getUnit_timeout())); + + TimeUnit timoutUnit = Utils.getTimoutUnit(request.getUnit_timeout()); + int timout = Utils.checkUserTimeout(request.getTimeout(), SERVICES_TIMEOUT_CAUSAL, timoutUnit); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Pair resultAndExecutionTime; + try { + var future = executor.submit(() -> processCommand(request)); + resultAndExecutionTime = Utils.runServicesWithTimeout(future, timout, timoutUnit); + } catch (TimeoutException e) { + LoggerUtil.logger.info("Timeout while running causal reasoner."); + return new CausalReasonerResponse( + null, + request.getEmail(), + timout, + request.getUnit_timeout(), + TIMEOUT + ); + } catch (ExecutionException e) { + LoggerUtil.logger.warning(() -> "Error while running causal reasoner: " + e.getMessage()); + e.printStackTrace(); + throw new RuntimeException(e); + } catch (InterruptedException e) { + LoggerUtil.logger.warning(() -> "Interrupt while running causal reasoner: " + e.getMessage()); + e.printStackTrace(); + Thread.currentThread().interrupt(); + throw new RuntimeException("Thread was interrupted."); + } finally { + executor.shutdownNow(); + } + + long executionTime = resultAndExecutionTime.getValue(); + String result = resultAndExecutionTime.getKey(); + return new CausalReasonerResponse( + result, + request.getEmail(), + executionTime, + request.getUnit_timeout(), + SUCCESS + ); + } + + private String processCommand(CausalReasonerPost causalReasonerPost) { + return switch (causalReasonerPost.getCmd()) { + case GET_CONCLUSIONS -> processConclusionsCommand(causalReasonerPost); + case GET_SIGNIFICANT_ATOMS -> processSignificantAtomsCommand(causalReasonerPost); + case GET_ARGUMENTATION_FRAMEWORK -> processArgumentationFramework(causalReasonerPost); + case GET_SEQUENCE_EXPLANATIONS -> processSequenceExplanations(causalReasonerPost); + }; + } + + private String processConclusionsCommand(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + var conclusionFilter = parseConclusionFilter(causalReasonerPost); + + Collection conclusions = causalReasonerService.queryConclusions(causalKnowledgeBase, observations, conclusionFilter); + return conclusions.toString(); + } + + private String processSignificantAtomsCommand(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + var conclusionFilter = parseConclusionFilter(causalReasonerPost); + + var perAtomSignificantAtoms = causalReasonerService.queryPerAtomSignificantAtoms(causalKnowledgeBase, observations, conclusionFilter); + + Map> jsonData = new HashMap<>(); + for (Map.Entry> entry : perAtomSignificantAtoms.entrySet()) { + List list = new ArrayList<>(); + for (Proposition proposition : entry.getValue()) { + String string = proposition.toString(); + list.add(string); + } + jsonData.put(entry.getKey().toString(), list); + } + + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonData); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private String processSequenceExplanations(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + var conclusionFilter = parseConclusionFilter(causalReasonerPost); + + var result = causalReasonerService.querySequenceExplanations(causalKnowledgeBase, observations, conclusionFilter); + var reply = SequenceExplanationReply.from(result); + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(reply); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private String processArgumentationFramework(CausalReasonerPost causalReasonerPost) { + CausalKnowledgeBase causalKnowledgeBase = parseCausalKnowledgeBase(causalReasonerPost); + Collection observations = parseObservations(causalReasonerPost); + + var result = causalReasonerService.queryArgumentationFramework(causalKnowledgeBase, observations); + var reply = ArgumentationFrameworkReply.from(result); + try { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(reply); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private static Collection parseObservations(CausalReasonerPost causalReasonerPost) { + CausalParser causalParser = new CausalParser(); + Collection observations; + try { + observations = causalParser.parseListOfFormulae(causalReasonerPost.getObservations(), ","); + } catch (ParserException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, null, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return observations; + } + + private static @Nullable Set parseConclusionFilter(CausalReasonerPost causalReasonerPost) { + return ConclusionsFilterSerialization.parse(causalReasonerPost.getConclusionsFilter()); + } + + private static CausalKnowledgeBase parseCausalKnowledgeBase(CausalReasonerPost causalReasonerPost) { + CausalParser causalParser = new CausalParser(); + CausalKnowledgeBase causalKnowledgeBase; + try { + causalKnowledgeBase = causalParser.parseBeliefBase(causalReasonerPost.getKb()); + } catch (ParserException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, null, e); + } catch (IOException e) { + throw new RuntimeException(e); + } + return causalKnowledgeBase; + } } diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentationFrameworkReply.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentationFrameworkReply.java new file mode 100644 index 000000000..506744cd5 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ArgumentationFrameworkReply.java @@ -0,0 +1,56 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.tweetyproject.arg.dung.syntax.Argument; +import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.web.services.sequenceexplanation.AttackDTO; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class ArgumentationFrameworkReply { + private final List arguments; + private final List attacks; + + public ArgumentationFrameworkReply(List arguments, List attacks) { + this.arguments = arguments; + this.attacks = attacks; + } + + public List getArguments() { + return arguments; + } + + public List getAttacks() { + return attacks; + } + + public static ArgumentationFrameworkReply from(DungTheory argumentationFramework) { + var arguments = argumentationFramework.stream() + .map(Argument::getName) + .collect(Collectors.toUnmodifiableList()); + var attacksConverted = AttackDTO.from(argumentationFramework.getAttacks()); + return new ArgumentationFrameworkReply(arguments, attacksConverted); + } +} + diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java new file mode 100644 index 000000000..5a6d59ec4 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerPost.java @@ -0,0 +1,184 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.lang.Nullable; + +import javax.validation.constraints.NotNull; + +/** + * Request to execute a {@link CausalReasonerPost#cmd} with a {@link org.tweetyproject.causal.reasoner.AbstractCausalReasoner} + * + * @author Oleksandr Dzhychko + */ +public final class CausalReasonerPost { + + public CausalReasonerPost( + @JsonProperty(value = "cmd", required = true) + Cmd cmd, + @JsonProperty("email") + String email, + @JsonProperty(value = "kb", required = true) + String kb, + @JsonProperty(value = "observations", required = true) + String observations, + @JsonProperty(value = "conclusions_filter") + String conclusionsFilter, + @JsonProperty(value = "timeout", required = true) + int timeout, + @JsonProperty(value = "unit_timeout", required = true) + String unit_timeout + ) { + this.cmd = cmd; + this.email = email; + this.kb = kb; + this.observations = observations; + this.conclusionsFilter = conclusionsFilter; + this.timeout = timeout; + this.unit_timeout = unit_timeout; + } + + /** + * Describes which command should be executed by the causal reasoner + */ + public enum Cmd { + /** + * Instructs the reasoner to calculate the conclusions + * + * @see org.tweetyproject.causal.reasoner.AbstractCausalReasoner#getConclusions + */ + @JsonProperty("get_conclusions") GET_CONCLUSIONS, + + /** + * Instructs the reasoner to calculate per atom the atoms which are significant for its conclusion. + */ + @JsonProperty("get_significant_atoms") GET_SIGNIFICANT_ATOMS, + + /** + * Instructs the reasoner to calculate the corresponding argumentation framework. + */ + @JsonProperty("get_argumentation_framework") GET_ARGUMENTATION_FRAMEWORK, + + /** + * Instructs the reasoner to calculate the sequence of explanations for all consequences. + */ + @JsonProperty("get_sequence_explanations") GET_SEQUENCE_EXPLANATIONS; + } + + /** + * The command type for the reasoner request + */ + @NotNull + private Cmd cmd; + + /** + * The email associated with the request + */ + @Nullable + private String email; + + /** + * The knowledge base (KB) for the reasoner request + * The format of the knowledge base must be as described in {@link org.tweetyproject.causal.parser.CausalParser#parseBeliefBase(java.io.Reader)} + */ + @NotNull + private String kb; + + /** + * The observations for the reasoner request + * The format of the knowledge base must be as used by {@link org.tweetyproject.causal.parser.CausalParser#parseListOfFormulae} with "," (comma) as delimiter. + */ + @NotNull + private String observations; + + /** + * Atoms for which the conclusions should be queried and returned. + * The format of the knowledge base must be as described in {@link ConclusionsFilterSerialization#parse(String)} + */ + @Nullable + private String conclusionsFilter; + + /** + * The timeout in seconds for the reasoner request + */ + private int timeout; + + /** + * The unit timeout for the reasoner request + */ + @NotNull + private String unit_timeout; + + public Cmd getCmd() { + return this.cmd; + } + + public void setCmd(Cmd cmd) { + this.cmd = cmd; + } + + public String getEmail() { + return this.email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getKb() { + return this.kb; + } + + public void setKb(String kb) { + this.kb = kb; + } + + public String getObservations() { + return this.observations; + } + + public void setObservations(String observations) { + this.observations = observations; + } + + public String getConclusionsFilter() { + return this.conclusionsFilter; + } + + public void setConclusionsFilter(String conclusionsFilter) { + this.conclusionsFilter = conclusionsFilter; + } + + public int getTimeout() { + return this.timeout; + } + + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + public String getUnit_timeout() { + return this.unit_timeout; + } + + public void setUnit_timeout(String unit_timeout) { + this.unit_timeout = unit_timeout; + } +} \ No newline at end of file diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerResponse.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerResponse.java new file mode 100644 index 000000000..8f27e59fd --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerResponse.java @@ -0,0 +1,86 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.springframework.lang.NonNull; + +/** + * Response to {@link CausalReasonerPost} + * + * @author Oleksandr Dzhychko + */ +public final class CausalReasonerResponse { + + public enum Status { + SUCCESS, + TIMEOUT, + } + + /** + * Result of execution {@link CausalReasonerPost#getCmd()} if {@link CausalReasonerResponse#status} is {@link Status#SUCCESS}. + * Else {@code null}. + */ + private final String reply; + /** + * E-Mail (or other identifier) as provided by {@link CausalReasonerPost#getEmail()} + */ + private final String email; + /** + * Time it took execute the command + */ + private final double time; + /** + * The time unit of {@link CausalReasonerResponse#time} + */ + @NonNull + private final String unit_timeout; + /** + * Whether the execution executed successfully or timed out. + */ + private final Status status; + + public CausalReasonerResponse(String reply, String email, double time, @NonNull String unit_timeout, Status status) { + this.reply = reply; + this.email = email; + this.time = time; + this.status = status; + this.unit_timeout = unit_timeout; + } + + public String getReply() { + return reply; + } + + public String getEmail() { + return email; + } + + public double getTime() { + return time; + } + + @NonNull + public String getUnit_timeout() { + return unit_timeout; + } + + public Status getStatus() { + return status; + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java new file mode 100644 index 000000000..43e2f3265 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/CausalReasonerService.java @@ -0,0 +1,111 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Service; +import org.tweetyproject.arg.dung.syntax.Attack; +import org.tweetyproject.arg.dung.syntax.DungTheory; +import org.tweetyproject.arg.explanations.reasoner.acceptance.DialecticalSequenceExplanationReasoner; +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; +import org.tweetyproject.causal.reasoner.ArgumentationBasedCausalReasoner; +import org.tweetyproject.causal.syntax.CausalKnowledgeBase; +import org.tweetyproject.logics.pl.syntax.PlFormula; +import org.tweetyproject.logics.pl.syntax.Proposition; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +@Service +public final class CausalReasonerService { + private final ArgumentationBasedCausalReasoner causalReasoner = new ArgumentationBasedCausalReasoner(); + private final DialecticalSequenceExplanationReasoner explanationReasoner = new DialecticalSequenceExplanationReasoner(); + + public Collection queryConclusions(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { + Collection conclusions = causalReasoner.getConclusions(causalKnowledgeBase, observations); + conclusions = filterConclusions(conclusions, conclusionFilter); + return conclusions; + } + + private static Collection filterConclusions(Collection conclusions, @Nullable Set conclusionFilter) { + if (conclusionFilter == null) { + return conclusions; + } + return conclusions.stream() + .filter(formula -> isConclusionInFilter(formula, conclusionFilter)) + .collect(Collectors.toUnmodifiableList()); + } + + public static boolean isConclusionInFilter(PlFormula conclusion, @NonNull Set conclusionFilter) { + return conclusionFilter.stream() + .anyMatch(proposition -> conclusion.getAtoms().contains(proposition)); + } + + + public Map> queryPerAtomSignificantAtoms(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { + return causalReasoner.getSignificantAtoms(causalKnowledgeBase, observations, Map.of(), conclusionFilter); + } + + public SequenceExplanations querySequenceExplanations(CausalKnowledgeBase causalKnowledgeBase, Collection observations, Set conclusionFilter) { + var theory = causalReasoner.getInducedTheory(causalKnowledgeBase, observations, Map.of()); + var perAtomArgumentsWithAtomInConclusion = causalReasoner.getPerAtomArgumentsWithAtomInConclusion(theory, conclusionFilter); + + var perAtomPerSequenceExplanations = new LinkedHashMap>(); + for (var atomWithArgumentsAtomInConclusion : perAtomArgumentsWithAtomInConclusion.entrySet()) { + var atom = atomWithArgumentsAtomInConclusion.getKey(); + var allSequenceExplanations = new ArrayList(); + perAtomPerSequenceExplanations.put(atom, allSequenceExplanations); + for (var argument : atomWithArgumentsAtomInConclusion.getValue().stream().findFirst().stream().collect(Collectors.toUnmodifiableList())) { + var explanations = explanationReasoner.getExplanations(theory, argument); + var sequenceExplanations = explanations.stream() + .map(explanation -> (DialectialSequenceExplanation) explanation) + .collect(Collectors.toUnmodifiableList()); + allSequenceExplanations.addAll(sequenceExplanations); + } + } + return new SequenceExplanations(theory.getAttacks(), perAtomPerSequenceExplanations); + } + + public static final class SequenceExplanations { + private final Set attacks; + private final Map> perAtomSequenceExplanations; + + public SequenceExplanations(Set attacks, + Map> perAtomSequenceExplanations) { + this.attacks = attacks; + this.perAtomSequenceExplanations = perAtomSequenceExplanations; + } + + public Set getAttacks() { + return attacks; + } + + public Map> getPerAtomSequenceExplanations() { + return perAtomSequenceExplanations; + } + } + + public DungTheory queryArgumentationFramework(CausalKnowledgeBase causalKnowledgeBase, Collection observations) { + return causalReasoner.getInducedTheory(causalKnowledgeBase, observations, Map.of()); + } +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java new file mode 100644 index 000000000..1584fa3d0 --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/ConclusionsFilterSerialization.java @@ -0,0 +1,75 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.springframework.http.HttpStatus; +import org.springframework.lang.Nullable; +import org.springframework.web.server.ResponseStatusException; +import org.tweetyproject.causal.parser.CausalParser; +import org.tweetyproject.logics.pl.syntax.PlFormula; +import org.tweetyproject.logics.pl.syntax.Proposition; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public class ConclusionsFilterSerialization { + + private static final String ATOM_DELIMITER = ","; + private static final CausalParser CAUSAL_PARSER = new CausalParser(); + + /** + * Parse the filter string for conclusions. + * + * @param conclusionsFilterString {@link Proposition}s as parsable by {@link CausalParser#parseFormula(String)} seperated by {@link #ATOM_DELIMITER} + * @return Set of {@link Proposition}s or {@code null} if the input is {@code null} or empty. + */ + public static @Nullable Set parse(@Nullable String conclusionsFilterString) { + if (conclusionsFilterString == null) { + return null; + } + List formulae; + try { + formulae = CAUSAL_PARSER.parseListOfFormulae(conclusionsFilterString, ATOM_DELIMITER); + } catch (RuntimeException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, null, e); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + var propositions = formulae.stream() + .map(formula -> { + if (formula instanceof Proposition) { + return (Proposition) formula; + } + String msg = String.format("Formula `%s` is not a proposition,", formula); + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, msg); + }) + .collect(Collectors.toUnmodifiableSet()); + + if (propositions.isEmpty()) { + return null; + } + return propositions; + }; +} diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java new file mode 100644 index 000000000..7f869248a --- /dev/null +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/causal/SequenceExplanationReply.java @@ -0,0 +1,67 @@ +/* + * This file is part of "TweetyProject", a collection of Java libraries for + * logical aspects of artificial intelligence and knowledge representation. + * + * TweetyProject is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services.causal; + +import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; +import org.tweetyproject.logics.pl.syntax.Proposition; +import org.tweetyproject.web.services.sequenceexplanation.AttackDTO; +import org.tweetyproject.web.services.sequenceexplanation.DialectialSequenceExplanationDTO; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Oleksandr Dzhychko + */ +public final class SequenceExplanationReply { + private final List attacks; + private final Map> perAtomSequenceExplanations; + + public SequenceExplanationReply(List attacks, Map> perAtomSequenceExplanations) { + this.attacks = attacks; + this.perAtomSequenceExplanations = perAtomSequenceExplanations; + } + + + public List getAttacks() { + return attacks; + } + + public Map> getPerAtomSequenceExplanations() { + return perAtomSequenceExplanations; + } + + public static SequenceExplanationReply from(CausalReasonerService.SequenceExplanations sequenceExplanations) { + var attacksConverted = AttackDTO.from(sequenceExplanations.getAttacks()); + var perAtomSequenceExplanationsConverted = from(sequenceExplanations.getPerAtomSequenceExplanations()); + return new SequenceExplanationReply(attacksConverted, perAtomSequenceExplanationsConverted); + } + + private static Map> from(Map> perAtomSequenceExplanations) { + return perAtomSequenceExplanations.entrySet().stream() + .collect(Collectors.toMap( + entry -> entry.getKey().toString(), + entry -> DialectialSequenceExplanationDTO.from(entry.getValue()), + (sequences1, sequences2) -> { throw new IllegalStateException("Encountered duplicate serialization of proposition."); }, + LinkedHashMap::new)); + } +} + diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java index cb1451af9..8cdcda78c 100644 --- a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/DialectialSequenceExplanationDTO.java @@ -19,6 +19,7 @@ package org.tweetyproject.web.services.sequenceexplanation; import org.tweetyproject.arg.explanations.semantics.DialectialSequenceExplanation; +import org.tweetyproject.web.util.ArgumentSerialization; import java.util.List; import java.util.stream.Collectors; diff --git a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentSerialization.java b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/util/ArgumentSerialization.java similarity index 96% rename from org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentSerialization.java rename to org-tweetyproject-web/src/main/java/org/tweetyproject/web/util/ArgumentSerialization.java index ac814916f..d66ac1408 100644 --- a/org-tweetyproject-web/src/main/java/org/tweetyproject/web/services/sequenceexplanation/ArgumentSerialization.java +++ b/org-tweetyproject-web/src/main/java/org/tweetyproject/web/util/ArgumentSerialization.java @@ -16,7 +16,7 @@ * * Copyright 2025 The TweetyProject Team */ -package org.tweetyproject.web.services.sequenceexplanation; +package org.tweetyproject.web.util; import org.tweetyproject.arg.dung.syntax.Argument; @@ -44,4 +44,4 @@ public static List> fromCollectionOfCollections(List. + * + * Copyright 2025 The TweetyProject Team + */ +package org.tweetyproject.web.services; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +/** + * @author Oleksandr Dzhychko + */ +@SpringBootTest +@AutoConfigureMockMvc +class RequestControllerCausalTest { + @Autowired + private MockMvc mvc; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Test + public void causalReasonerWithInvalidKnowledgeBaseReturnsStatus400() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_conclusions", + "kb": "a <=> (b\\nc <=> d\\n{ d, !b }", + "observations": "!a, !b", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isBadRequest()); + } + + @Test + public void causalReasonerWithInvalidObservationsReturnsStatus400() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_conclusions", + "kb": "a <=> b\\nc <=> d\\n{ d, !b }", + "observations": "!(a, !b", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isBadRequest()); + } + + @Test + public void causalReasonerRepliesWithAllConclusions() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_conclusions", + "kb": "a <=> b\\nc <=> d\\n{ d, !b }", + "observations": "!a, !b", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "reply": "[!a, !b, c, d]", + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void causalReasonerRepliesWithFilteredConclusions() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_conclusions", + "kb": "a <=> b\\nc <=> d\\n{ d, !b }", + "observations": "!a, !b", + "conclusionsFilter": "b, c, e", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "reply": "[!b, c]", + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void causalReasonerCalculatesSignificantAtoms() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_significant_atoms", + "kb": "a <=> b\\nc <=> d\\n{ d, !b }", + "observations": "!a, !b", + "conclusionsFilter": "a", + "timeout": 10, + "unit_timeout": "s" + } + """); + + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(""" + { + "reply": "{\\n \\"a\\" : [ \\"a\\", \\"b\\" ]\\n}", + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, true)); + } + + @Test + public void causalReasonerGetSequenceExplanations() throws Exception { + var post = post("/causal") + .contentType(MediaType.APPLICATION_JSON) + .content(""" + { + "email": "aId", + "cmd": "get_sequence_explanations", + "kb": "a <=> b\\n{ b, !b }", + "observations": "", + "conclusionsFilter": "a", + "timeout": 10, + "unit_timeout": "s" + } + """); + + + var expectedReplyJSON = """ + { + "attacks" : [ { + "attacker" : "([b] -> b)", + "attacked" : "([!b] -> !a)" + }, { + "attacker" : "([!b] -> !b)", + "attacked" : "([b] -> b)" + }, { + "attacker" : "([b] -> b)", + "attacked" : "([!b] -> !b)" + }, { + "attacker" : "([!b] -> !b)", + "attacked" : "([b] -> a)" + } ], + "perAtomSequenceExplanations" : { + "a" : [ { + "argument" : "([b] -> a)", + "supporters" : [ [ "([b] -> b)" ], [ "([b] -> a)" ] ], + "defeated" : [ [ "([!b] -> !b)" ], [ ] ] + } ] + } + }"""; + var expectedReplyJSONEscaped = objectMapper.writeValueAsString(expectedReplyJSON); + var expectedResponse = String.format(""" + { + "reply": %s, + "email": "aId", + "time": 0, + "unit_timeout": "s", + "status": "SUCCESS" + } + """, expectedReplyJSONEscaped); + mvc.perform(post) + .andExpect(status().isOk()) + .andExpect(content().json(expectedResponse, true)); + } +} \ No newline at end of file