From f60809087dfb83ed513cfbcdd177f48c4d61d6dd Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:29:57 +0200 Subject: [PATCH 01/32] Adding tetrad and junit 5 dependencies --- pom.xml | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index 3e3466f..8f30b34 100644 --- a/pom.xml +++ b/pom.xml @@ -55,12 +55,12 @@ - - io.github.cmu-phil - tetrad-lib - - 7.6.4 - + + io.github.cmu-phil + tetrad-lib + + 7.6.4 + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + @@ -123,10 +130,23 @@ + org.apache.maven.plugins maven-surefire-plugin - 2.22.2 + 3.1.2 + + + org.junit.platform + junit-platform-engine + 1.10.0 + + + org.junit.jupiter + junit-jupiter-engine + 5.10.0 + + From 17d57dd9ad2b8b45c269a2f4f61fe0fcc6aca349 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Thu, 10 Jul 2025 10:30:24 +0200 Subject: [PATCH 02/32] Cleaning and testing AlphaOrder --- .../uclm/i3a/simd/consensusBN/AlphaOrder.java | 355 +++++++++--------- .../i3a/simd/consensusBN/AlphaOrderTest.java | 119 ++++++ 2 files changed, 296 insertions(+), 178 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/AlphaOrderTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java b/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java index 55e7e2c..7865c15 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java @@ -3,152 +3,157 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; + import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; -import edu.cmu.tetrad.graph.Edges; import edu.cmu.tetrad.graph.Endpoint; import edu.cmu.tetrad.graph.Node; +/** + * This class implements a heuristic to compute an ancestral order of nodes for a set of DAGs. + * The heuristic is based on finding the best sink node in each iteration for the set of DAGs, + * removing it from the DAGs, and repeating the process until all nodes are ordered. + */ public class AlphaOrder { - ArrayList setOfDags = null; - ArrayList alpha = null; - ArrayList setOfauxG = null; -// ArrayList dpaths = null; + /** + * The set of DAGs to compute the ancestral order from. + */ + private final ArrayList setOfDags; + /** + * The computed ancestral order of nodes. + */ + private ArrayList alpha; + /** + * A set of auxiliary DAGs used during the computation. + */ + private final ArrayList setOfauxG; + /** + * Constructor for the AlphaOrder class. + * Initializes the set of DAGs and creates a copy of each DAG to work with. + * @param dags the list of DAGs from which to compute the ancestral order. + * This constructor creates a deep copy of each DAG to avoid modifying the original DAGs during + * the computation of the ancestral order. + */ public AlphaOrder(ArrayList dags){ - + // Check if the dags are valid + checkExceptions(dags); + + // Initialize the class variables this.setOfDags = dags; - this.alpha = new ArrayList(); - this.setOfauxG = new ArrayList(); -// this.dpaths = new ArrayList(); - + this.alpha = new ArrayList<>(); + this.setOfauxG = new ArrayList<>(); for (Dag i : setOfDags) { Dag aux_G = new Dag(i); setOfauxG.add(aux_G); -// dpaths.add(computeDirectedPathFromTo(aux_G)); } - } - - public int[][] computeDirectedPathFromTo(Dag graph) { - LinkedList dpathNewEdges = new LinkedList(); - dpathNewEdges.clear(); - dpathNewEdges.addAll(graph.getEdges()); - List dpathNodes = null; - dpathNodes = graph.getNodes(); - - int numNodes = dpathNodes.size(); - int [][] dpath = new int[numNodes][numNodes]; + + /** + * Checks for exceptions in the input set of DAGs. + * Throws an IllegalArgumentException if the set is null, empty, or contains DAGs with different nodes. + * Also checks that the size of the set is greater than 1. + * @param setOfDags the set of DAGs to check for exceptions. + */ + private void checkExceptions(ArrayList setOfDags) { + // Check if setOfDags is null + if(setOfDags == null) { + throw new IllegalArgumentException("The set of DAGs is null."); + } + + // Check if all DAGs have the same nodes + if (setOfDags.isEmpty()) { + throw new IllegalArgumentException("The set of DAGs is empty."); + } + // Check that the size is greater than 1 + if(setOfDags.size() <= 1) { + throw new IllegalArgumentException("The set of DAGs has only one DAG."); + } - while (!dpathNewEdges.isEmpty()) { - Edge edge = dpathNewEdges.removeFirst(); - Node _nodeT = Edges.getDirectedEdgeTail(edge); - Node _nodeH = Edges.getDirectedEdgeHead(edge); - int _indexT = dpathNodes.indexOf(_nodeT); - int _indexH = dpathNodes.indexOf(_nodeH); - dpath[_indexT][_indexH] = 1; - int dPathT = 0; - int dPathH = 0; - int mindPath = 0; - for (int i = 0; i < dpathNodes.size(); i++) { - dPathT = dpath[i][_indexT]; - if (dpath[i][_indexT] >= 1) { - dPathH = dpath[i][_indexH]; - if(dPathH == 0) dpath[i][_indexH] = dPathT+1; - else{ - mindPath = Math.min(dPathH, dPathT+1); - dpath[i][_indexH]=mindPath; - } - } - dPathH = dpath[_indexH][i]; - if(dpath[_indexH][i] >= 1){ - dPathT = dpath[_indexT][i]; - if(dPathT ==0) dpath[_indexT][i] = dPathH+1; - else{ - mindPath = Math.min(dPathT, dPathH+1); - dpath[_indexT][i] = mindPath; - } - - } + // Check that all DAGs have the same nodes + List firstDagNodes = setOfDags.get(0).getNodes(); + for (Dag dag : setOfDags) { + if (!dag.getNodes().equals(firstDagNodes)) { + throw new IllegalArgumentException("All DAGs must have the same nodes. Dag " + dag + " has different nodes than the rest of DAGs."); } } - return dpath; - } + } + + /** + * Returns the nodes of the first DAG in the set, since all DAGs are assumed to have the same nodes. + * @return + */ public List getNodes(){ return(setOfDags.get(0).getNodes()); } - // heursitica para orden de conceso basada en el numero de caminos dirigidos. (Es muy mala no se utiliza) - - public void computeAlphaH1(){ - - List nodes = setOfDags.get(0).getNodes(); - LinkedList alpha = new LinkedList(); - - while(nodes.size()>0){ - int index_alpha = computeNextH1(nodes); - Node node_alpha = nodes.get(index_alpha); - alpha.addFirst(node_alpha); - for(Dag g: this.setOfauxG){ - removeNode(g,node_alpha); - //int[][] newDpaths = computeDirectedPathFromTo(g); -// this.dpaths.set(this.setOfauxG.indexOf(g), newDpaths); - } - nodes.remove(node_alpha); - } - this.alpha = new ArrayList(alpha); - } - - // heuistica para encontrar un orden de conceso. Se basa en los enlaces que generaria seguir una secuencia creada desde los nodos sumideros hacia arriba. - -public void computeAlphaH2(){ + /** + * This method computes the heuristic to find an ancestral order of nodes of consensus. It is based on the number of edges that would be added on a sequence created from the sink nodes upwards. + * It iteratively finds the node with the minimum number of changes (inversions and additions of edges) and adds it to the beginning of the order. + * */ + public void computeAlpha(){ + // Get nodes and initialize the alpha list List nodes = setOfDags.get(0).getNodes(); - LinkedList alpha = new LinkedList(); + LinkedList alpha_aux = new LinkedList<>(); - while(nodes.size()>0){ - int index_alpha = computeNextH2(nodes); - Node node_alpha = nodes.get(index_alpha); - alpha.addFirst(node_alpha); + while(!nodes.isEmpty()){ + int index_alpha = computeNextSink(nodes); + Node nodeAlpha = nodes.get(index_alpha); + alpha_aux.addFirst(nodeAlpha); for(Dag g: this.setOfauxG){ - removeNode(g,node_alpha); + removeNode(g,nodeAlpha); } - nodes.remove(node_alpha); + nodes.remove(nodeAlpha); } - this.alpha = new ArrayList(alpha); + this.alpha = new ArrayList<>(alpha_aux); } - - int computeNextH2(List nodes){ + /** + * Gets the following node in the order based on the minimum number of changes (inversions and additions of edges) that would be required to create a sequence from the sink nodes upwards. + * @param nodes Remaining nodes to be ordered. + * @return index of the node that should be added next to the order. + */ + private int computeNextSink(List nodes){ - int changes = 0; + // Setting up variables to count changes + int changes; int inversion = 0; int addition = 0; int indexNode = 0; int min = Integer.MAX_VALUE; - + + // Iterate through each node to find the one with the minimum changes for the list of DAGs. for(int i=0; i inserted = new ArrayList(); + // Checking total amount of inversions. We add -1 to give relevance to nodes that are already sinks. List children = g.getChildren(nodei); inversion += (children.size()-1); + + // Checking edge additions from parents of each child to nodei and from parents of nodei to children. + ArrayList inserted = new ArrayList<>(); List paX = g.getParents(nodei); for(Node child: children){ List paY = g.getParents(child); + // For each parent of nodei, check if it has an edge to the child for(Node nodep: paX){ - if(g.getEdge(nodep, child)==null){ - addition++; - } + if(g.getEdge(nodep, child)==null){ + addition++; + } } + // For each parent of the child, check if it has an edge to nodei for(Node nodec: paY){ if(!nodec.equals(nodei)){ + // If there is no edge between nodec and nodei, we consider adding it if((g.getEdge(nodec,nodei)==null) && (g.getEdge(nodei,nodec)==null)){ Edge toBeInserted = new Edge(nodec,nodei,Endpoint.CIRCLE,Endpoint.CIRCLE); boolean contains = false; + // Checking if we have already added this edge to the list of inserted edges + // to avoid counting it multiple times. for(Edge e: inserted){ if((e.getNode1().equals(nodec) && (e.getNode2().equals(nodei))) || ((e.getNode1().equals(nodei) && (e.getNode2().equals(nodec))))){ @@ -156,6 +161,7 @@ int computeNextH2(List nodes){ break; } } + // Checkin if there is a new edge addition, we update the counter and the list of inserted edges if so. if(!contains){ addition++; inserted.add(toBeInserted); @@ -165,117 +171,110 @@ int computeNextH2(List nodes){ } } } + // Calculate total changes for the current node changes = inversion + addition; + // If the current node has less changes than the minimum found so far, we update the minimum and the index of the node + // to be added to the order. if(changes < min){ min = changes; indexNode = i; } - changes = 0; + // Resetting changes for the next iteration inversion = 0; addition = 0; } return indexNode; } - void removeNode(Dag g, Node node_alpha){ + /** + * Removes a node from the DAG and updates the edges according to a new node added to the alpha order. + * It removes a sink node and updates the edges to maintain the directed paths in the DAG. + * This is done each iteration of the heuristic to compute the alpha order. + * @param g the DAG from which the node is to be removed. + * @param nodeAlpha the node to be removed from the DAG. + */ + private void removeNode(Dag g, Node nodeAlpha){ - List children = g.getChildren(node_alpha); + List children = g.getChildren(nodeAlpha); while(!children.isEmpty()){ - int i=0; - Node child; - boolean seguir = false; - do{ - child = children.get(i++); - g.removeEdge(node_alpha, child); - seguir=false; - if(g.paths().existsDirectedPath(node_alpha,child)){ - seguir=true; - g.addEdge(new Edge(node_alpha,child,Endpoint.TAIL, Endpoint.ARROW)); - } - }while(seguir); - - List paX = g.getParents(node_alpha); - List paY = g.getParents(child); - paY.remove(node_alpha); - g.addEdge(new Edge(child,node_alpha,Endpoint.TAIL, Endpoint.ARROW)); - for(Node nodep: paX){ - Edge pay = g.getEdge(nodep, child); - if(pay == null) - g.addEdge(new Edge(nodep,child,Endpoint.TAIL,Endpoint.ARROW)); + // 1. Select a child that prevents a cycle when nodeAlpha <- child is added. + Node child = selectChild(g, nodeAlpha, children); - } - for(Node nodep : paY){ - Edge paz = g.getEdge(nodep,node_alpha); - if(paz == null) - g.addEdge(new Edge(nodep,node_alpha,Endpoint.TAIL,Endpoint.ARROW)); - } + // 2. Cover the edge nodeAlpha -> child by adding edges from parents of nodeAlpha to child and from parents of child to nodeAlpha. Last of all we revert the edge nodeAlpha -> child. + // This is done to maintain the directed paths in the DAG. + coverEdge(g, nodeAlpha, child); + // 3. Delete the child from the list of children of nodeAlpha, as it has been processed. children.remove(child); } - g.removeNode(node_alpha); + // Finally, remove the nodeAlpha from the DAG. + g.removeNode(nodeAlpha); } + /** + * Selects a child node from the list of children of nodeAlpha that does not create a cycle when an edge from nodeAlpha to the child is added (nodeAlpha <- child). + * @param g the DAG from which the child is to be selected. + * @param nodeAlpha the node from the alpha order heuristic. + * @param children the remaining children of nodeAlpha in the DAG. + * @return the selected child node that does not create a cycle when an edge from nodeAlpha to the child is added. + */ + private Node selectChild(Dag g, Node nodeAlpha, List children) { + int i=0; + Node child; + boolean endCondition; + do{ + child = children.get(i++); + g.removeEdge(nodeAlpha, child); + endCondition=false; + if(g.paths().existsDirectedPath(nodeAlpha,child)){ + endCondition=true; + g.addEdge(new Edge(nodeAlpha,child,Endpoint.TAIL, Endpoint.ARROW)); + } + }while(endCondition); + return child; + } - int computeNextH1(List nodes){ + /** + * Covers the edge from nodeAlpha to child by adding edges from parents of nodeAlpha to child and from parents of child to nodeAlpha. + * This is done to maintain the directed paths in the DAG after removing nodeAlpha. + * @param g the DAG where the edge is to be covered. + * @param nodeAlpha the node from the alpha order heuristic. + * @param child the child node selected from the list of children of nodeAlpha. + */ + private void coverEdge(Dag g, Node nodeAlpha, Node child) { + // Getting the parents of nodeAlpha and child. + List paX = g.getParents(nodeAlpha); + List paY = g.getParents(child); + paY.remove(nodeAlpha); - int min = Integer.MAX_VALUE; - int minIndex = 0; - - for(int i=0 ; i< nodes.size(); i++){ - int weightNodei = 0; - //for(Dag dag : this.setOfauxG){ - // int[][] dpath = this.dpaths.get(this.setOfauxG.indexOf(dag)); - // for(int j=0 ; j child. + g.addEdge(new Edge(child,nodeAlpha,Endpoint.TAIL, Endpoint.ARROW)); } + + - public ArrayList getOrder(){ - + /** + * Returns the computed ancestral order of nodes. + * @return an ArrayList of nodes representing the ancestral order of the DAGs after applying the alpha order heuristic. + */ + public ArrayList getOrder(){ return this.alpha; } - - - public static void main(String args[]) { - -// ArrayList dags = new ArrayList(); -// ArrayList alfa = new ArrayList(); -// -// -// System.out.println("Grafos de Partida: "); -// System.out.println("---------------------"); -//// Graph graph = GraphConverter.convert("X1-->X5,X2-->X3,X3-->X4,X4-->X1,X4-->X5"); -//// Dag dag = new Dag(graph); -// -// Dag dag = new Dag(); -// // dag = GraphUtils.randomDag(Integer.parseInt(args[0]), Integer.parseInt(args[1]), true); -// dags.add(dag); -// System.out.println("DAG: ---------------"); -// System.out.println(dag.toString()); -// for (int i=0 ; i < Integer.parseInt(args[2])-1 ; i++){ -// // Dag newDag = GraphUtils.randomDag(dag.getNodes(),Integer.parseInt(args[1]) ,true); -// dags.add(newDag); -// System.out.println("DAG: ---------------"); -// System.out.println(newDag.toString()); -// } -// -// AlphaOrder order = new AlphaOrder(dags); -// order.computeAlphaH2(); -// alfa = order.getOrder(); -// -// System.out.println("Orden de Consenso: " + alfa.toString()); - - - } - - } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/AlphaOrderTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/AlphaOrderTest.java new file mode 100644 index 0000000..e965021 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/AlphaOrderTest.java @@ -0,0 +1,119 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +class AlphaOrderTest { + + private Node a, b, c; + private Dag dag1, dag2; + private ArrayList dags; + + @BeforeEach + void setup() { + a = new GraphNode("A"); + b = new GraphNode("B"); + c = new GraphNode("C"); + + // DAG 1: A → B → C + dag1 = new Dag(); + dag1.addNode(a); + dag1.addNode(b); + dag1.addNode(c); + dag1.addDirectedEdge(a, b); + dag1.addDirectedEdge(b, c); + + // DAG 2: A → B, A → C + dag2 = new Dag(); + dag2.addNode(a); + dag2.addNode(b); + dag2.addNode(c); + dag2.addDirectedEdge(a, b); + dag2.addDirectedEdge(a, c); + + dags = new ArrayList<>(Arrays.asList(dag1, dag2)); + } + + @Test + void constructorThrowsOnNullInput() { + assertThrows(IllegalArgumentException.class, () -> new AlphaOrder(null)); + } + + @Test + void constructorThrowsOnEmptyList() { + assertThrows(IllegalArgumentException.class, () -> new AlphaOrder(new ArrayList<>())); + } + + @Test + void constructorThrowsOnSingleDAG() { + ArrayList singleDagList = new ArrayList<>(); + singleDagList.add(dag1); + assertThrows(IllegalArgumentException.class, () -> new AlphaOrder(singleDagList)); + } + + @Test + void constructorThrowsOnDifferentNodes() { + // Crear otro DAG con nodos diferentes + Dag dagDifferent = new Dag(); + dagDifferent.addNode(new GraphNode("X")); + dagDifferent.addNode(new GraphNode("Y")); + dagDifferent.addDirectedEdge(dagDifferent.getNode("X"), dagDifferent.getNode("Y")); + + ArrayList badList = new ArrayList<>(Arrays.asList(dag1, dagDifferent)); + assertThrows(IllegalArgumentException.class, () -> new AlphaOrder(badList)); + } + + @Test + void computeAlphaReturnsValidOrder() { + AlphaOrder alphaOrder = new AlphaOrder(dags); + alphaOrder.computeAlpha(); + List order = alphaOrder.getOrder(); + + assertNotNull(order); + assertEquals(3, order.size()); + assertTrue(order.contains(a)); + assertTrue(order.contains(b)); + assertTrue(order.contains(c)); + + // Optional: check for uniqueness + assertEquals(3, order.stream().distinct().count()); + } + + @Test + void computeAlphaForTwoSimpleDags(){ + AlphaOrder alphaOrder = new AlphaOrder(dags); + alphaOrder.computeAlpha(); + List order = alphaOrder.getOrder(); + + // Basic assertions to check the order + assertNotNull(order); + assertEquals(3, order.size()); + assertTrue(order.contains(a)); + assertTrue(order.contains(b)); + assertTrue(order.contains(c)); + + // Check that the order is A, B, C + assertEquals(a, order.get(0)); + assertEquals(b, order.get(1)); + assertEquals(c, order.get(2)); + + // Check for uniqueness + assertEquals(3, order.stream().distinct().count()); + + } +} + + + From 04a11306319f438b314f34e877390d26ac976e74 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Sat, 12 Jul 2025 19:04:16 +0200 Subject: [PATCH 03/32] Cleaning and testing BetaToAlpha --- .../i3a/simd/consensusBN/BetaToAlpha.java | 383 +++++++++++------- .../i3a/simd/consensusBN/BetaToAlphaTest.java | 93 +++++ 2 files changed, 323 insertions(+), 153 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BetaToAlpha.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BetaToAlpha.java index 6398bf8..86e3856 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BetaToAlpha.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BetaToAlpha.java @@ -4,48 +4,90 @@ import java.util.HashMap; import java.util.List; import java.util.Random; -import edu.cmu.tetrad.graph.Node; + import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; import edu.cmu.tetrad.graph.Endpoint; +import edu.cmu.tetrad.graph.Node; - +/** + * BetaToAlpha is a class that transforms a directed acyclic graph (DAG) into an I-map minimal with respect to a specified alpha order. + * It constructs a compatible beta order and modifies the graph accordingly. + * The transformation respects the alpha order, ensuring that the resulting graph is consistent with it. + */ public class BetaToAlpha { - Dag G = null; - ArrayList beta = new ArrayList(); - ArrayList alfa = new ArrayList(); - HashMap alfaHash= new HashMap(); - Dag G_aux = null; - int numberOfInsertedEdges = 0; + /** + * The directed acyclic graph (DAG) to be transformed. + */ + private final Dag G; + + /** + * The beta order derived from the alpha order. + */ + private List beta; + + /** + * The alpha order that the graph should respect. In consensusBN, this alpha order has been created using the AlphaOrder class. + * If null, a random order will be created. + */ + private List alpha; - public BetaToAlpha(Dag G, ArrayList alfa){ + /** + * A hash map to store the index of each node in the alpha order for quick access. + */ + private final HashMap alphaHash= new HashMap<>(); + + /** + * The auxiliary graph used during the transformation process. + */ + private Dag G_aux = null; - this.alfa = alfa; + /** + * The number of edges inserted during the transformation process. + */ + int numberOfInsertedEdges = 0; + + /** + * Constructor for BetaToAlpha that initializes the graph and alpha order. + * @param G the directed acyclic graph (DAG) to be transformed. + * @param alpha the alpha order that the graph should respect. + */ + public BetaToAlpha(Dag G, ArrayList alpha){ + this.alpha = alpha; this.G = G; this.beta = null; - for(int i= 0; i< alfa.size(); i++){ - Node n = alfa.get(i); - alfaHash.put(n, i); + for(int i= 0; i< alpha.size(); i++){ + Node n = alpha.get(i); + alphaHash.put(n, i); } } + /** + * Constructor for BetaToAlpha that initializes the graph without a specified alpha order. + * A random alpha order will need to be created. + * @param G + */ public BetaToAlpha(Dag G){ - - this.alfa = null; + this.alpha = null; this.G = G; this.beta = null; - } - void computeAlfaHash(){ + /** + * Computes the alpha hash map if it is not already computed. + * This method populates the alphaHash with the index of each node in the alpha order. + * It is called before any transformation to ensure that the alpha order is respected. + * If the alpha order is null, it will not compute the hash. + */ + public void computeAlphaHash(){ - if(this.alfa !=null){ - if(alfaHash.isEmpty()){ - for(int i= 0; i< alfa.size(); i++){ - Node n = alfa.get(i); - alfaHash.put(n, i); + if(this.alpha !=null){ + if(alphaHash.isEmpty()){ + for(int i= 0; i< alpha.size(); i++){ + Node n = alpha.get(i); + alphaHash.put(n, i); } } } @@ -55,10 +97,15 @@ void computeAlfaHash(){ // Only to test the methods, to build a random order. - public ArrayList randomAlfa (Random aleatorio){ + /** + * Builds a random alpha order from the nodes of the graph. This is used for test purposes to ensure that the transformation can handle different orders. + * @param aleatorio the random number generator to use for shuffling the nodes. + * @return a list of nodes representing a random alpha order. + */ + public List randomAlfa (Random aleatorio){ List nodes = this.G.getNodes(); - this.alfa = new ArrayList(); + this.alpha = new ArrayList<>(); int[] index = new int[nodes.size()]; @@ -76,82 +123,121 @@ public ArrayList randomAlfa (Random aleatorio){ } for (int i = 0; i< nodes.size(); i++){ - this.alfa.add(i, nodes.get(index[i])); + this.alpha.add(i, nodes.get(index[i])); } - this.computeAlfaHash(); - return this.alfa; + this.computeAlphaHash(); + return this.alpha; } - + /** + * Transforms the graph G into an I-map minimal with respect to the alpha order. + */ public void transform(){ + // 1. Create a compatible beta order with the alfa order for the DAG G. + buildBetaOrder(); + + // 2. Transform graph G into an I-map minimal with alpha order + transformWithBeta(); + + } + + /** + * Builds the beta order that best respects the alpha order for the given graph G. + * This method constructs a beta order by identifying sink nodes and arranging them in a way that minimizes the number of edges that violate the alpha order. + * It uses a greedy approach to select the next node based on its position in the alpha order. + * The beta order is constructed such that it is as close as possible to the alpha order while ensuring that the resulting graph is still a DAG. + * + * This method modifies the G_aux graph to reflect the current state of the transformation. + * It also initializes the beta list with the first sink node and iteratively adds nodes to the beta order based on their relationships in the graph. + */ + private void buildBetaOrder() { this.G_aux = new Dag(this.G); - this.beta = new ArrayList(); + this.beta = new ArrayList<>(); + List parents; + + // Compute the sink nodes and add the first one to beta. ArrayList sinkNodes = getSinkNodes(this.G_aux); this.beta.add(sinkNodes.get(0)); - List pa = G_aux.getParents(sinkNodes.get(0)); + parents = G_aux.getParents(sinkNodes.get(0)); this.G_aux.removeNode(sinkNodes.get(0)); sinkNodes.remove(0); + // Compute the new sink nodes - for(Node nodep: pa){ - List chld = G_aux.getChildren(nodep); - if (chld.size() == 0) sinkNodes.add(nodep); - } + updateSinkNodes(sinkNodes, parents); - // Construct beta order as closer as possible to alfa. - + // Construct beta order as close as possible to alpha. while (this.G_aux.getNumNodes()>0){ - // sinkNodes = getSinkNodes(this.G_aux); + // Select fist sink node Node sink = sinkNodes.get(0); - pa = G_aux.getParents(sink); + parents = G_aux.getParents(sink); this.G_aux.removeNode(sink); sinkNodes.remove(0); // Compute the new sink nodes - for(Node nodep: pa){ - List chld = G_aux.getChildren(nodep); - if (chld.size() == 0) sinkNodes.add(nodep); - } + updateSinkNodes(sinkNodes, parents); - int index_alfa_sink = this.alfaHash.get(sink); //this.alfa.indexOf(sink); - boolean ok = true; - int i = 0; - - while(ok){ - - Node nodej = this.beta.get(i); - int index_alfa_nodej = this.alfaHash.get(nodej); //this.alfa.indexOf(nodej); - - if (index_alfa_nodej > index_alfa_sink){ ok = false; break;} - if (this.G.getParents(nodej).contains(sink)){ ok = false; break;} - if (i == this.beta.size()-1){ ok = false; break;} - i++; + // Compute the index to insert the sink node in beta. + int insertIndex = 0; + for (; insertIndex < beta.size(); insertIndex++) { + Node current = beta.get(insertIndex); + if (alphaHash.get(current) > alphaHash.get(sink)) break; + if (G.getParents(current).contains(sink)) break; } - - this.beta.add(i,sink); + beta.add(insertIndex, sink); } + } +/* FUTURE IDEA: SELECT BEST SINK NODE FROM ALPHA ORDER. + private Node selectBestSinkNode(List sinkNodes) { + return sinkNodes.stream() + .min(Comparator.comparingInt(alfaHash::get)) + .orElse(sinkNodes.get(0)); + } +*/ + /** + * Updates the sink nodes list based on the current list of candidates. + * This method checks each candidate node to see if it has any children in the auxiliary graph G_aux. + * If a candidate node has no children, it is added to the sink nodes list. + * This is used to maintain the integrity of the beta order during the transformation process. + * + * @param sinkNodes the list of current sink nodes to be updated. + * @param candidates the list of candidate nodes to check for children. + */ + private void updateSinkNodes(ArrayList sinkNodes, List candidates) { + // Compute the new sink nodes + for(Node node: candidates){ + List chld = G_aux.getChildren(node); + if (chld.isEmpty()) + sinkNodes.add(node); + } + } + + /** + * Transforms the graph G into an I-map minimal with respect to the alpha order. + * This method rearranges the edges in the graph based on the beta order derived from the alpha order. + * It ensures that the resulting graph respects the alpha order by checking the relationships between nodes and adjusting edges accordingly. + * The transformation modifies the graph in place and updates the beta list to reflect the new order of nodes. + */ + private void transformWithBeta() { + ArrayList orderedNodes = new ArrayList<>(); + // Setting the first node in the orderedNodes list. + orderedNodes.add(this.beta.remove(0)); - // transform graph G into an I-map minimal with alpha order - - ArrayList aux_beta = new ArrayList(); - aux_beta.add(this.beta.get(0)); - this.beta.remove(0); - - while(this.beta.size()>0){ // check each variable from the sink nodes. - - aux_beta.add(this.beta.get(0)); + while(!this.beta.isEmpty()){ + // Setting the next node in the orderedNodes list. + orderedNodes.add(this.beta.get(0)); this.beta.remove(0); - int i = aux_beta.size(); - boolean ok = true; + int i = orderedNodes.size(); + boolean changed = true; - while (ok){ - + while (changed){ if(i==1) break; - ok = false; - Node nodeY = aux_beta.get(i-1); - Node nodeZ = aux_beta.get(i-2); - -// if ((nodeZ != null) && (this.alfa.indexOf(nodeZ) > this.alfa.indexOf(nodeY))){ - if ((nodeZ != null) && (this.alfaHash.get(nodeZ) > this.alfaHash.get(nodeY))){ + changed = false; + // Getting the last two nodes in the ordered list + Node nodeY = orderedNodes.get(i-1); + Node nodeZ = orderedNodes.get(i-2); + + // Check if there is an edge from nodeZ to nodeY, if so, cover it. + if ((nodeZ != null) && (this.alphaHash.get(nodeZ) > this.alphaHash.get(nodeY))){ if(this.G.getEdge(nodeZ, nodeY) != null){ List paZ = this.G.getParents(nodeZ); List paY = this.G.getParents(nodeY); @@ -173,96 +259,87 @@ public void transform(){ } } } - ok = true; - aux_beta.remove(nodeY); - aux_beta.add(i-2,nodeY); + changed = true; + orderedNodes.remove(nodeY); + orderedNodes.add(i-2,nodeY); i--; } } } - - this.beta = aux_beta; - + this.beta = orderedNodes; } - + /** + * Returns the number of edges that were inserted during the transformation process. + * This method is useful for understanding how many modifications were made to the original graph to achieve the desired alpha order. + * @return + */ public int getNumberOfInsertedEdges(){ - return this.numberOfInsertedEdges; } - ArrayList getSinkNodes(Dag g){ - - ArrayList sourcesNodes = new ArrayList(); + /** + * Retrieves the sink nodes from the given directed acyclic graph (DAG). + * A sink node is defined as a node that does not have any children in the graph. + * This method iterates through all nodes in the graph and checks their children to determine if they are sink nodes. + * + * @param g the directed acyclic graph (DAG) from which to retrieve sink nodes. + * @return an ArrayList of sink nodes that do not have any children in the graph. + */ + private ArrayList getSinkNodes(Dag g){ + // Get nodes from DAG + ArrayList sinkNodes = new ArrayList<>(); List nodes = g.getNodes(); - - for (Node nodei : nodes){ - if(g.getChildren(nodei).isEmpty()) sourcesNodes.add(nodei); + // Check which nodes don't have children and add them to sinkNodes + for (Node node : nodes){ + if(g.getChildren(node).isEmpty()){ + sinkNodes.add(node); + } } - return sourcesNodes; - + return sinkNodes; } - - - -// public static void main(String args[]) { -// -// //Graph graph = GraphConverter.convert("X1-->X2,X1-->X3,X2-->X4,X3-->X4"); -// Graph graph = GraphConverter.convert("X2-->X1,X3-->X1,X1-->X4,X5-->X4,X4-->X6"); -// Dag dag = new Dag(graph); -// -// Dag dag2 = GraphUtils.randomDag(dag.getNodes(), 7, true); -//// BayesPm bayesPm = new BayesPm(dag, 3, 3); -//// MlBayesIm bayesIm = new MlBayesIm(bayesPm); -//// -//// Element element = BayesXmlRenderer.getElement(bayesIm); -//// System.out.println("Started with this bayesIm: " + bayesIm); -//// System.out.println("\nGot this XML for it:"); -//// Document xmldoc = new Document(element); -//// Serializer serializer = new Serializer(System.out); -//// serializer.setLineSeparator("\n"); -//// serializer.setIndent(2); -//// try { -//// serializer.write(xmldoc); -//// } -//// catch (IOException e) { -//// throw new RuntimeException(e); -//// } -// -// -// System.out.println(GraphUtils.graphToDot(dag)); -// -// -//// System.out.println("Dag Inicial: "+ dag.toString()); -// -// Random aleatorio = new Random(150); -// BetaToAlpha mt = new BetaToAlpha(dag); -// mt.randomAlfa (aleatorio); -// mt.transform(); -//// System.out.println(mt.G.toString()+" Alfa: "+mt.alfa.toString()+" Beta: "+ mt.beta.toString() ); -// -// System.out.println(GraphUtils.graphToDot(mt.G)); -// -// -// -//// System.out.println("Dag Inicial: "+ dag2.toString()); -// -// System.out.println(GraphUtils.graphToDot(dag2)); -// -// BetaToAlpha mt2 = new BetaToAlpha(dag2); -// Random aleat2 = new Random(150); -// mt2.randomAlfa(aleat2); -// mt2.transform(); -// -//// System.out.println(mt2.G.toString()+" Alfa: "+mt2.alfa.toString()+" Beta: "+ mt2.beta.toString() ); -// -// System.out.println(GraphUtils.graphToDot(mt2.G)); -// -// -// -// } - - + /** + * Returns the alpha hash map that contains the index of each node in the alpha order. + * This map is used to quickly access the position of nodes in the alpha order during the transformation process. + * It is particularly useful for ensuring that the resulting graph respects the specified alpha order. + * @return the alpha hash map where keys are nodes and values are their indices in the alpha order. + */ + public HashMap getAlphaHash() { + return alphaHash; + } + + /** + * Sets the alpha order for the transformation. + * This method allows the user to specify a new alpha order for the graph transformation. + * It updates the alpha field and recomputes the alpha hash map to reflect the new order. + * @param alpha the new alpha order to be set for the transformation. + */ + public void setAlphaOrder(List alpha) { + this.alpha = alpha; + this.computeAlphaHash(); + } + + /** + * Returns the alpha order that the graph should respect. + * @return the alpha order as a list of nodes, or null if no alpha order has been set. + */ + public List getAlphaOrder() { + return alpha; } + /** + * Returns the directed acyclic graph (DAG) that has been transformed. + * This method provides access to the modified graph after the transformation has been applied. + * The graph will be an I-map minimal with respect to the specified alpha order. + * + * @see BetaToAlpha#transform() + * @return the transformed directed acyclic graph (DAG) as a Dag object. + */ + public Dag getGraph() { + return G; + } + + +} + diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java new file mode 100644 index 0000000..7d421ad --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java @@ -0,0 +1,93 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; +import java.util.List; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Edge; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +public class BetaToAlphaTest { + + private Dag dag; + private Node a, b, c, d; + private ArrayList alphaOrder; + + @BeforeEach + void setUp() { + a = new GraphNode("A"); + b = new GraphNode("B"); + c = new GraphNode("C"); + d = new GraphNode("D"); + + // DAG: A → B, A → C, B → D, C → D + dag = new Dag(); + dag.addNode(a); + dag.addNode(b); + dag.addNode(c); + dag.addNode(d); + dag.addDirectedEdge(a, b); + dag.addDirectedEdge(a, c); + dag.addDirectedEdge(b, d); + dag.addDirectedEdge(c, d); + + // Define an alpha order that requires modifying the graph + alphaOrder = new ArrayList<>(Arrays.asList(d, c, b, a)); + } + + @Test + void testTransformRespectsAlphaOrder() { + BetaToAlpha bta = new BetaToAlpha(dag, alphaOrder); + bta.transform(); + + // El grafo debería haber invertido al menos algunos arcos + assertTrue(bta.getNumberOfInsertedEdges() > 0); + + // Validamos que el orden resultante es compatible con alpha + for (Edge edge : dag.getEdges()) { + Node from = edge.getNode1(); + Node to = edge.getNode2(); + + int fromIndex = alphaOrder.indexOf(from); + int toIndex = alphaOrder.indexOf(to); + + assertTrue(fromIndex < toIndex, "Edge violates alpha order: " + from + " → " + to); + } + } + + @Test + void testRandomAlphaProducesPermutation() { + BetaToAlpha bta = new BetaToAlpha(dag); + List randomAlpha = bta.randomAlfa(new Random(42)); + + assertNotNull(randomAlpha); + assertEquals(dag.getNumNodes(), randomAlpha.size()); + + Set originalNodes = new HashSet<>(dag.getNodes()); + Set shuffled = new HashSet<>(randomAlpha); + + assertEquals(originalNodes, shuffled); // misma colección, diferente orden + } + + @Test + void testComputeAlphaHashBuildsCorrectMap() { + BetaToAlpha bta = new BetaToAlpha(dag, alphaOrder); + bta.computeAlphaHash(); + + for (int i = 0; i < alphaOrder.size(); i++) { + Node node = alphaOrder.get(i); + assertEquals(i, (bta.getAlphaHash()).get(node)); + } + } +} From f12c2475336ce6de1c1867c7bd49f01ff4b8cb21 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Mon, 14 Jul 2025 12:05:31 +0200 Subject: [PATCH 04/32] Cleaning and testing TransformDags --- .../i3a/simd/consensusBN/TransformDags.java | 278 +++++++----------- .../simd/consensusBN/TransformDagsTest.java | 104 +++++++ 2 files changed, 207 insertions(+), 175 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/TransformDagsTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/TransformDags.java b/src/main/java/es/uclm/i3a/simd/consensusBN/TransformDags.java index 9861e99..f798234 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/TransformDags.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/TransformDags.java @@ -2,205 +2,133 @@ import java.util.ArrayList; - -import edu.cmu.tetrad.graph.Node; import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Node; +/** + * This class transforms a set of DAGs by applying the BetaToAlpha transformation to each DAG with a given alpha order. + */ public class TransformDags { + /** + * List of input DAGs to be transformed. + */ + private final ArrayList setOfDags; + /** + * List of output DAGs after transformation. + */ + private final ArrayList setOfOutputDags; + /** + * The alpha order used for the transformation. + */ + private final ArrayList alpha; + /** + * The transformation objects for each DAG. + * Each BetaToAlpha object applies the transformation to a corresponding DAG in setOfDags using the alpha order provided. + */ + private ArrayList transformers= null; - ArrayList setOfDags = null; - ArrayList setOfOutputDags = null; - ArrayList alfa = null; - ArrayList metAs= null; - int numberOfInsertedEdges = 0; -// int weight[][][] = null; + /** + * Number of edges inserted during the transformation process. + * This is used to track how many edges were added to the transformed DAGs. + */ + private int numberOfInsertedEdges = 0; - public TransformDags(ArrayList dags, ArrayList alfa){ + /** + * Constructor for TransformDags. + * Initializes the object with a list of DAGs and an alpha order. + * It creates a new BetaToAlpha transformation for each DAG in the input list. + * Each DAG in the input list will be transformed according to this alpha order. + * The transformation is applied by creating a BetaToAlpha object for each DAG. + * The transformed DAGs will be stored in setOfOutputDags after calling the transform() method. + * @see BetaToAlpha + * @param dags List of DAGs to be transformed. + * @param alpha List of nodes representing the alpha order for the transformation. + */ + public TransformDags(ArrayList dags, ArrayList alpha){ this.setOfDags = dags; - this.setOfOutputDags = new ArrayList(); - this.metAs = new ArrayList(); - this.alfa = alfa; - + this.setOfOutputDags = new ArrayList<>(); + this.transformers = new ArrayList<>(); + this.alpha = alpha; + // Initialize the BetaToAlpha transformation for each DAG in the input list for (Dag i : setOfDags) { Dag out = new Dag(i); - this.metAs.add(new BetaToAlpha(out,alfa)); + this.transformers.add(new BetaToAlpha(out,this.alpha)); } } - + /** + * Transforms the input DAGs by applying the BetaToAlpha transformation. + * This method iterates through each BetaToAlpha object, applies the transformation, + * and collects the transformed DAGs into setOfOutputDags. + * It also counts the total number of edges inserted during the transformations. + * + * @see BetaToAlpha#transform() + * @see BetaToAlpha#getNumberOfInsertedEdges() + * @see BetaToAlpha#getGraph() + * @return An ArrayList of transformed DAGs after applying the BetaToAlpha transformation. + */ public ArrayList transform (){ - this.numberOfInsertedEdges = 0; - - for(BetaToAlpha transformDagi: this.metAs){ + for(BetaToAlpha transformDagi: this.transformers){ transformDagi.transform(); this.numberOfInsertedEdges += transformDagi.getNumberOfInsertedEdges(); - this.setOfOutputDags.add(transformDagi.G); + this.setOfOutputDags.add(transformDagi.getGraph()); } - - return this.setOfOutputDags; - } + /** + * Returns the number of edges that were inserted during the transformation process. + * @return The total number of edges inserted across all transformed DAGs. + */ public int getNumberOfInsertedEdges(){ return this.numberOfInsertedEdges; } + /** + * Returns the list of input DAGs that were transformed. + * @return An ArrayList of DAGs that were provided as input to the transformation. + */ + public ArrayList getSetOfDags() { + return this.setOfDags; + } + + /** + * Returns the list of transformed output DAGs. + * This list contains the DAGs after applying the BetaToAlpha transformation. + * @return An ArrayList of transformed DAGs. + */ + public ArrayList getSetOfOutputDags() { + return this.setOfOutputDags; + } + + /** + * Returns the alpha order used for the transformation. + * @return An ArrayList of nodes representing the alpha order. + */ + public ArrayList getAlpha() { + return this.alpha; + } + + /** + * Returns the list of BetaToAlpha transformers used for the transformation. + * @return An ArrayList of BetaToAlpha objects, each corresponding to a DAG in the input list. + */ + public ArrayList getTransformers() { + return this.transformers; + } + + /** + * Sets the list of BetaToAlpha transformers. + * This method allows for updating the transformers used in the transformation process. + * + * @param transformers An ArrayList of BetaToAlpha objects to be set as the transformers for this TransformDags instance. + * Each transformer will apply the BetaToAlpha transformation to its corresponding DAG in the input list. + */ + public void setTransformers(ArrayList transformers) { + this.transformers = transformers; + } - - -// void computeWeight(){ -// -// this.weight = new int[this.setOfDags.size()][this.alfa.size()][this.alfa.size()]; -// -// for(Dag g: this.setOfOutputDags){ -// for(Node nodei : g.getNodes()){ -// List pa = g.getParents(nodei); -// if(pa.isEmpty()) continue; -// List anc = new ArrayList(); -// anc.add(nodei); -// anc = g.getAncestors(anc); -// Dag gAnc = new Dag(g.subgraph(anc)); -// // me quedo con el grafo ancestral del node_i -// for(Node pai: pa){ // Calculo el numero de caminos desde los ancestros que se "activan" borrando cada padre. -// int npaths = 0; -// Dag gAncNopai = new Dag(gAnc); -// for(Node rm : pa) if(!rm.equals(pai)) gAncNopai.removeNode(rm); // borro todos los padres menos el pa_i en el grafo ancestral. -// for(Node nodeAc: anc){ // para cada ancestro voy mirando si hay un camino dirigido. -// if((gAncNopai.getNodes().contains(nodeAc))&&(!nodeAc.equals(nodei))) -// if(GraphUtils.paths().existsDirectedPath(gAncNopai,nodeAc, nodei)) npaths++; -// } -// // npaths tiene el numero de caminos diridos que se han activado quitando el padre pa_i -// this.weight[this.setOfOutputDags.indexOf(g)][g.getNodes().indexOf(nodei)][g.getNodes().indexOf(pai)] = npaths; -// } -// -// } -// -// } -// -// } - -// public Dag computeWeightDag(boolean w){ -// -// Dag wDag = new Dag(this.alfa); -// for(Node nodei : this.alfa){ -// for(Node nodej : this.alfa){ -// if(nodei.equals(nodej)) continue; -// int wij = 0; -// for(Dag g: this.setOfOutputDags){ -// int wg = this.weight[this.setOfOutputDags.indexOf(g)][g.getNodes().indexOf(nodei)][g.getNodes().indexOf(nodej)]; -// if(wg > 0 && !w) wg = 1; -// else if (wg == 0) wg =-1; -// wij+=wg; -// } -// if(wij > 0) wDag.addEdge(new Edge(nodej,nodei,Endpoint.TAIL,Endpoint.ARROW)); -// } -// } -// return wDag; -// } -// -// public static void main(String args[]) { -// -// ArrayList dags = new ArrayList(); -// ArrayList alfa = new ArrayList(); -// Random aleatorio = new Random(222); -// -// -// System.out.println("Grafos de Partida: "); -// System.out.println("---------------------"); -//// Graph graph = GraphConverter.convert("X1-->X5,X2-->X3,X3-->X4,X4-->X1,X4-->X5"); -//// Dag dag = new Dag(graph); -// -// Dag dag = new Dag(); -// -// dag = GraphUtils.randomDag(Integer.parseInt(args[0]), Integer.parseInt(args[1]), true); -// BetaToAlpha mt = new BetaToAlpha(dag); -// alfa = mt.randomAlfa(aleatorio); -// dags.add(dag); -// System.out.println("DAG: ---------------"); -// System.out.println(dag.toString()); -// for (int i=0 ; i < Integer.parseInt(args[2])-1 ; i++){ -// Dag newDag = GraphUtils.randomDag(dag.getNodes(),Integer.parseInt(args[1]) ,true); -// dags.add(newDag); -// System.out.println("DAG: ---------------"); -// System.out.println(newDag.toString()); -// } -// -// -// -// System.out.println("Orden de Consenso: " + alfa.toString()); -// -// TransformDags setOfDags = new TransformDags(dags,alfa); -// setOfDags.transform(); -// -// -// -// -// for(Dag d : setOfDags.setOfOutputDags){ -// System.out.println("DAG trasformado: ---------------"); -// System.out.println(d.toString()); -// } -// -// -// -// Dag union = new Dag(alfa); -// -// for(Node nodei: alfa){ -// for(Dag d : setOfDags.setOfOutputDags){ -// Listparent = d.getParents(nodei); -// for(Node pa: parent){ -// if(!union.isParentOf(pa, nodei)) union.addEdge(new Edge(pa,nodei,Endpoint.TAIL,Endpoint.ARROW)); -// } -// } -// -// } -// -// -// System.out.println("Grafo UNION: "+union.toString()); -// setOfDags.computeWeight(); -// Dag wDag = setOfDags.computeWeightDag(true); -// System.out.println("Grafo Consenso: "+ wDag.toString()); -// Dag wDag2 = setOfDags.computeWeightDag(false); -// System.out.println("Grafo Consenso sin pesos: "+ wDag2.toString()); -// -// -// -// -// -// -// -//// Node nod = dag.getNodes().get(aleatorio.nextInt(alfa.size())); -//// ArrayList a = new ArrayList(); -//// a.add(nod); -//// List anc = dag.getAncestors(a); -//// -//// System.out.println("Ancenstros de " + nod.toString()+ " "+anc.toString()); -//// -//// System.out.println("Subgraph: "+ dag.subgraph(anc)); -//// -//// List pa = dag.getParents(nod); -//// System.out.println("padres de: "+nod.toString()+ " : "+pa.toString()); -//// Node pai = pa.get(aleatorio.nextInt(pa.size())); -//// System.out.println("Padre elegido a borrar: "+pai.toString()); -//// pa.remove(pai); -//// -//// -//// -//// for(Node rm: pa) anc.remove(rm); -//// -//// Graph pp = dag.subgraph(anc); -//// int npath = 0; -//// for(Node ancestor: pp.getNodes()){ -//// if(!ancestor.equals(nod)){ -//// List> paths = GraphUtils.allPathsFromTo(pp, ancestor, nod); -//// if(dag.paths().existsDirectedPath(ancestor, nod)) npath++; -//// System.out.println("Caminos desde: "+ancestor.toString()+" a: "+nod.toString()+" : "+paths.toString()); -//// } -//// } -//// System.out.println(" El numero de caminos desde hacia: "+ nod+" es de: "+npath); -// } -// } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/TransformDagsTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/TransformDagsTest.java new file mode 100644 index 0000000..b399ed3 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/TransformDagsTest.java @@ -0,0 +1,104 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +public class TransformDagsTest { + + private ArrayList inputDags; + private ArrayList alpha; + + @BeforeEach + public void setUp() { + inputDags = new ArrayList<>(); + + // We use 4 nodes for the DAGs + Node nodeA = new GraphNode("A"); + Node nodeB = new GraphNode("B"); + Node nodeC = new GraphNode("C"); + Node nodeD = new GraphNode("D"); + + // Create first DAG with these edges: A -> B, A -> C, B -> D, C -> D + Dag dag1 = new Dag(); + dag1.addNode(nodeA); + dag1.addNode(nodeB); + dag1.addNode(nodeC); + dag1.addNode(nodeD); + + // Adding directed edges to the DAG + dag1.addDirectedEdge(nodeA, nodeB); + dag1.addDirectedEdge(nodeA, nodeC); + dag1.addDirectedEdge(nodeB, nodeD); + dag1.addDirectedEdge(nodeC, nodeD); + + // Adding the DAG to the list + inputDags.add(dag1); + + // Create second DAG with these edges: D -> C, D -> B, C -> A, B -> A + Dag dag2 = new Dag(); + dag2.addNode(nodeA); + dag2.addNode(nodeB); + dag2.addNode(nodeC); + dag2.addNode(nodeD); + + // Adding directed edges to the second DAG + dag2.addDirectedEdge(nodeD, nodeC); + dag2.addDirectedEdge(nodeD, nodeB); + dag2.addDirectedEdge(nodeC, nodeA); + dag2.addDirectedEdge(nodeB, nodeA); + + // Adding the second DAG to the list + inputDags.add(dag2); + + // Apply AlphaOrder algorithm to these dags: + AlphaOrder alphaOrder = new AlphaOrder(inputDags); + alphaOrder.computeAlpha(); + alpha = alphaOrder.getOrder(); + + } + + @Test + public void testConstructorInitializesCorrectly() { + TransformDags transformer = new TransformDags(inputDags, alpha); + + assertNotNull(transformer); + assertEquals(0, transformer.getNumberOfInsertedEdges()); + } + + @Test + public void testTransformReturnsCorrectSize() { + TransformDags transformer = new TransformDags(inputDags, alpha); + ArrayList result = transformer.transform(); + + assertNotNull(result); + assertEquals(inputDags.size(), result.size()); + } + + @Test + public void testTransformUpdatesNumberOfInsertedEdges() { + TransformDags transformer = new TransformDags(inputDags, alpha); + transformer.transform(); + + // No sabemos cuántas aristas se insertan exactamente sin saber cómo funciona BetaToAlpha, + // pero al menos podemos comprobar que el valor no es negativo. + assertTrue(transformer.getNumberOfInsertedEdges() >= 0); + } + + @Test + public void testEmptyDagListReturnsEmptyOutput() { + TransformDags transformer = new TransformDags(new ArrayList<>(), alpha); + ArrayList result = transformer.transform(); + + assertTrue(result.isEmpty()); + assertEquals(0, transformer.getNumberOfInsertedEdges()); + } +} From 71e5cbec9067b9d7b069c9d351e0144288f339ec Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:16:32 +0200 Subject: [PATCH 05/32] Cleaning and testing ConsensusUnion and updating pom dependencies --- pom.xml | 28 ++++ .../i3a/simd/consensusBN/ConsensusUnion.java | 131 ++++++++++++----- .../simd/consensusBN/ConsensusUnionTest.java | 138 ++++++++++++++++++ 3 files changed, 257 insertions(+), 40 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusUnionTest.java diff --git a/pom.xml b/pom.xml index 8f30b34..2af8d6d 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,13 @@ 5.10.0 test + + + org.apache.commons + commons-math3 + 3.6.1 + + @@ -149,6 +156,27 @@ + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + + prepare-agent + + + + report + test + + report + + + + + diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java index 8c38fd9..ba63e1b 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java @@ -6,59 +6,119 @@ import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; import edu.cmu.tetrad.graph.Endpoint; -import edu.cmu.tetrad.graph.Graph; import edu.cmu.tetrad.graph.Node; - +/** + * This class implements the Consensus Union algorithm which applies a fusion between multiple Directed Acyclic Graphs (DAGs). + * It constructs a consensus DAG by merging the input DAGs based on a specified order of nodes (alpha). + * The alpha order is computed with the AlphaOrder class which implements a Greedy Heuristic Order (GHO) search, achieving a good order to transform the input DAGs. + * Once each DAG is transformed, the union method creates a new DAG that contains all the edges from the input DAGs, ensuring that the resulting graph is acyclic. + * The number of edges inserted during the union process can be retrieved using getNumberOfInsertedEdges. + * + * This class is also runnable, allowing it to be executed in a separate thread. + */ public class ConsensusUnion implements Runnable{ - ArrayList alpha = null; - Dag outputDag = null; - AlphaOrder heuristic = null; - TransformDags imaps2alpha = null; - ArrayList setOfdags = null; + /** + * The alpha order of nodes in the consensus DAG. + * This order is used to transform the input DAGs into a compatible I-Maps before merging. + * It is computed using the AlphaOrder class. + * + * @see AlphaOrder + */ + private ArrayList alpha; + /** + * The AlphaOrder heuristic used to compute the alpha order. + */ + private AlphaOrder heuristic = null; + + /** + * The TransformDags instance that transforms the input DAGs based on the alpha order. + */ + private TransformDags imaps2alpha; + + /** + * List of input DAGs to be merged. + */ + private ArrayList setOfdags = null; + + /** + * The output DAG resulting from the union of the transformed input DAGs. + */ Dag union = null; + + /** + * Number of edges inserted during the consensus union process. + */ int numberOfInsertedEdges = 0; - + /** + * Constructor for ConsensusUnion that initializes the union process with a list of DAGs and an alpha order. + * @param dags the list of input DAGs to be merged. + * @param order the alpha order of nodes to be used for transforming the input DAGs. + */ public ConsensusUnion(ArrayList dags, ArrayList order){ this.setOfdags = dags; this.alpha = order; - } - - + /** + * Constructor for ConsensusUnion that initializes the union process with a list of DAGs and uses the AlphaOrder object to generate an alpha order. + * @see AlphaOrder + * @param dags the list of input DAGs to be merged. + */ public ConsensusUnion(ArrayList dags){ this.setOfdags = dags; this.heuristic = new AlphaOrder(this.setOfdags); - } + /** + * Default constructor for ConsensusUnion that initializes an empty union. + * This constructor can be used when the DAGs are set later using the setDags method. + */ public ConsensusUnion(){ this.setOfdags = null; } + /** + * Returns the number of edges inserted during the union process. + * This value is updated after the union method is called. + * @return the number of edges inserted in the consensus DAG. + */ public int getNumberOfInsertedEdges(){ return this.numberOfInsertedEdges; } + /** + * Performs the union of the input DAGs based on the alpha order. If no alpha order is set, it computes it first. + * The method transforms each input DAG according to the alpha order and then merges them into a single consensus DAG. + * The resulting DAG contains all edges from the transformed input DAGs, ensuring that it remains acyclic. + * + * @throws IllegalStateException if the alpha order is not set before calling this method. + * @throws IllegalArgumentException if the input DAGs are null or empty. + * @throws NullPointerException if the alpha order is null. + * @return the resulting consensus DAG after merging the transformed input DAGs. + * @see AlphaOrder + * @see TransformDags + */ public Dag union(){ + // Computing Alpha Order if not set, using the Greedy Heuristic Order (GHO) if(this.alpha == null){ - - this.heuristic.computeAlphaH2(); - this.alpha = this.heuristic.alpha; + this.heuristic.computeAlpha(); + this.alpha = this.heuristic.getOrder(); } + // Transforming each DAG with the alpha order this.imaps2alpha = new TransformDags(this.setOfdags,this.alpha); this.imaps2alpha.transform(); this.numberOfInsertedEdges = this.imaps2alpha.getNumberOfInsertedEdges(); + // Applying a union of the edges of the transformed DAGs this.union = new Dag(this.alpha); for(Node nodei: this.alpha){ - for(Dag d : this.imaps2alpha.setOfOutputDags){ + for(Dag d : this.imaps2alpha.getSetOfOutputDags()){ Listparent = d.getParents(nodei); for(Node pa: parent){ if(!this.union.isParentOf(pa, nodei)) this.union.addEdge(new Edge(pa,nodei,Endpoint.TAIL,Endpoint.ARROW)); @@ -70,43 +130,34 @@ public Dag union(){ } + /** + * Returns the resulting consensus DAG after the union process. + * This method should be called after the union method to ensure that the union has been performed. + * @return the consensus DAG resulting from the union of the input DAGs. + */ public Dag getUnion(){ return this.union; } + /** + * sets the list of input DAGs for the ConsensusUnion instance and applies the AlphaOrder heuristic to compute the alpha order. + * This method also updates the alpha order and transforms the input DAGs accordingly. + * @param dags + */ void setDags(ArrayList dags){ this.setOfdags = dags; this.heuristic = new AlphaOrder(this.setOfdags); - this.heuristic.computeAlphaH2(); - this.alpha = this.heuristic.alpha; + this.heuristic.computeAlpha(); + this.alpha = this.heuristic.getOrder(); this.imaps2alpha = new TransformDags(this.setOfdags,this.alpha); this.imaps2alpha.transform(); } - - - - public static void main(String args[]) { - - - System.out.println("Grafos de Partida: "); - - // (seed, n. variables, n egdes aprox, n. dags, mutation) - RandomBN setOfDags = new RandomBN(0, Integer.parseInt(args[0]), Integer.parseInt(args[1]), - Integer.parseInt(args[2]),Integer.parseInt(args[3])); - setOfDags.generate(); -// - for( Dag g: setOfDags.setOfRandomDags) System.out.print(g); - ConsensusUnion conDag= new ConsensusUnion(); - conDag.setDags(setOfDags.setOfRandomDags); - Graph g = conDag.union(); - System.out.println("grafo consenso: "+ g); - - } - - + /** + * Runs the ConsensusUnion process in a separate thread. + */ @Override public void run() { this.union = this.union(); diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusUnionTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusUnionTest.java new file mode 100644 index 0000000..e97c87f --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusUnionTest.java @@ -0,0 +1,138 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Graph; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +public class ConsensusUnionTest { + + private ArrayList inputDags; + private ArrayList alpha; + + @BeforeEach + public void setUp() { + inputDags = new ArrayList<>(); + + // We use 4 nodes for the DAGs + Node nodeA = new GraphNode("A"); + Node nodeB = new GraphNode("B"); + Node nodeC = new GraphNode("C"); + Node nodeD = new GraphNode("D"); + + // Create first DAG with these edges: A -> B, A -> C, B -> D, C -> D + Dag dag1 = new Dag(); + dag1.addNode(nodeA); + dag1.addNode(nodeB); + dag1.addNode(nodeC); + dag1.addNode(nodeD); + + // Adding directed edges to the DAG + dag1.addDirectedEdge(nodeA, nodeB); + dag1.addDirectedEdge(nodeA, nodeC); + dag1.addDirectedEdge(nodeB, nodeD); + dag1.addDirectedEdge(nodeC, nodeD); + + // Adding the DAG to the list + inputDags.add(dag1); + + // Create second DAG with these edges: D -> C, D -> B, C -> A, B -> A + Dag dag2 = new Dag(); + dag2.addNode(nodeA); + dag2.addNode(nodeB); + dag2.addNode(nodeC); + dag2.addNode(nodeD); + + // Adding directed edges to the second DAG + dag2.addDirectedEdge(nodeD, nodeC); + dag2.addDirectedEdge(nodeD, nodeB); + dag2.addDirectedEdge(nodeC, nodeA); + dag2.addDirectedEdge(nodeB, nodeA); + + // Adding the second DAG to the list + inputDags.add(dag2); + + // Apply AlphaOrder algorithm to these dags: + AlphaOrder alphaOrder = new AlphaOrder(inputDags); + alphaOrder.computeAlpha(); + alpha = alphaOrder.getOrder(); + + } + + @Test + public void testConstructorWithAlphaInitializesCorrectly() { + ConsensusUnion cu = new ConsensusUnion(inputDags, alpha); + assertNotNull(cu); + } + + @Test + public void testConstructorWithoutAlphaGeneratesAlpha() { + ConsensusUnion cu = new ConsensusUnion(inputDags); + assertNotNull(cu); + } + + @Test + public void testUnionReturnsNonNullDag() { + ConsensusUnion cu = new ConsensusUnion(inputDags, alpha); + Dag result = cu.union(); + assertNotNull(result); + } + + @Test + public void testNumberOfInsertedEdgesIsUpdated() { + ConsensusUnion cu = new ConsensusUnion(inputDags, alpha); + cu.union(); + assertTrue(cu.getNumberOfInsertedEdges() >= 0); + } + + @Test + public void testSetDagsUpdatesAlphaAndUnion() { + ConsensusUnion cu = new ConsensusUnion(); + cu.setDags(inputDags); + cu.union(); + Dag result = cu.getUnion(); + assertNotNull(result); + assertTrue(result.getNumEdges() >= 1); + } + + @Test + public void testRunMethodExecutesUnion() { + ConsensusUnion cu = new ConsensusUnion(inputDags, alpha); + cu.run(); + assertNotNull(cu.getUnion()); + } + + @Test + public void testEmptyDagListReturnsEmptyUnion() { + assertThrows(IllegalArgumentException.class, () -> new ConsensusUnion(new ArrayList<>())); + } + + @Test + public void testRandomBNGeneratesConsensusUnionCorrectly() { + + //System.out.println("Grafos de Partida: "); + + // (seed, n. variables, n egdes aprox, n. dags, mutation) + RandomBN setOfDags = new RandomBN(0, 20, 50, + 4,3); + setOfDags.generate(); + + //for( Dag g: setOfDags.setOfRandomDags) System.out.print(g); + ConsensusUnion conDag= new ConsensusUnion(setOfDags.setOfRandomDags); + Graph g = conDag.union(); + //System.out.println("grafo consenso: "+ g); + + assertNotNull(g); + assertTrue(g.getNumEdges() >= 0); + assertTrue(g.getNodes().size() == setOfDags.setOfRandomDags.get(0).getNodes().size()); + + } +} From 90688a85198d135c94260f1931b58141e9a9e210 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:35:12 +0200 Subject: [PATCH 06/32] Reformatring consensusBES and creating a new class for BES, consistency checked --- .../BackwardEquivalenceSearchDSep.java | 366 ++++++++++++++++++ .../i3a/simd/consensusBN/ConsensusBES.java | 41 +- .../simd/consensusBN/ConsensusBESTest.java | 124 ++++++ 3 files changed, 520 insertions(+), 11 deletions(-) create mode 100644 src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java new file mode 100644 index 0000000..079ce49 --- /dev/null +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -0,0 +1,366 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Edge; +import edu.cmu.tetrad.graph.EdgeListGraph; +import edu.cmu.tetrad.graph.Edges; +import edu.cmu.tetrad.graph.Endpoint; +import edu.cmu.tetrad.graph.Graph; +import edu.cmu.tetrad.graph.Node; +import edu.cmu.tetrad.search.utils.GraphSearchUtils; +import edu.cmu.tetrad.search.utils.MeekRules; +import static es.uclm.i3a.simd.consensusBN.Utils.pdagToDag; + +public class BackwardEquivalenceSearchDSep { + + private final Graph graph; + private final ArrayList transformedDags; + private final ArrayList initialDags; + private Dag outputDag; + private final Map localScore = new HashMap<>(); + private int numberOfInsertedEdges = 0; + + + public BackwardEquivalenceSearchDSep(Dag union, ArrayListinitialDags, ArrayList transformedDags) { + this.graph = new EdgeListGraph(new LinkedList<>(union.getNodes())); + for (Edge edge : union.getEdges()) { + graph.addEdge(edge); + } + this.initialDags = initialDags; + this.transformedDags = transformedDags; + } + + public Dag applyBackwardEliminationWithDSeparation(){ + // Implement the BESd algorithm logic here + // This is a placeholder for the actual BESd algorithm implementation + // The algorithm should modify the graph based on the BESd logic + rebuildPattern(graph); + Node x, y; + Set t = new HashSet<>(); + double score = 0; + double bestScore = score; + do { + x = y = null; + Set edges1 = graph.getEdges(); + List edges = new ArrayList<>(); + + for (Edge edge : edges1) { + Node _x = edge.getNode1(); + Node _y = edge.getNode2(); + + if (Edges.isUndirectedEdge(edge)) { + edges.add(Edges.directedEdge(_x, _y)); + edges.add(Edges.directedEdge(_y, _x)); + } else { + edges.add(edge); + } + } + for (Edge edge : edges) { + + Node _x = Edges.getDirectedEdgeTail(edge); + Node _y = Edges.getDirectedEdgeHead(edge); + + List hNeighbors = getHNeighbors(_x, _y, graph); +// List> hSubsets = powerSet(hNeighbors); + PowerSet hSubsets= PowerSetFabric.getPowerSet(_x,_y,hNeighbors); + + while(hSubsets.hasMoreElements()) { + SubSet hSubset=hSubsets.nextElement(); + double deleteEval = deleteEval(_x, _y, hSubset, graph); + if (!(deleteEval >= 1.0)) deleteEval = 0.0; + double evalScore = score + deleteEval; + + //System.out.println("Attempt removing " + _x + "-->" + _y + "(" +evalScore + ") "+ hSubset.toString()); + + if (!(evalScore > bestScore)) { + continue; + } + + // INICIO TEST 1 + List naYXH = findNaYX(_x, _y, graph); + naYXH.removeAll(hSubset); + if (!isClique(naYXH, graph)) { +// hSubsets.firstTest(true); // Si pasa para H entonces pasa para cualquier H' | H' contiene H + continue; + } + // FIN TEST 1 + + bestScore = evalScore; + x = _x; + y = _y; + t = hSubset; + } + + } + if (x != null) { + System.out.println(" "); + System.out.println("DELETE " + graph.getEdge(x, y) + t.toString() + " (" +bestScore + ")"); + System.out.println(" "); + delete(x, y, t, graph); + rebuildPattern(graph); + int deletedEdges = 0; + for(int g = 0; g *IMPORTANT!* *It assumes all colliders are oriented, as well as + * arrows dictated by time order.* + * + * ELIMINADO BACKGROUND KNOWLEDGE + */ + private void pdag(Graph graph) { + MeekRules rules = new MeekRules(); + rules.setMeekPreventCycles(true); + rules.orientImplied(graph); + } + + private static List getHNeighbors(Node x, Node y, Graph graph) { + List hNeighbors = new LinkedList<>(graph.getAdjacentNodes(y)); + hNeighbors.retainAll(graph.getAdjacentNodes(x)); + + for (int i = hNeighbors.size() - 1; i >= 0; i--) { + Node z = hNeighbors.get(i); + Edge edge = graph.getEdge(y, z); + if (!Edges.isUndirectedEdge(edge)) { + hNeighbors.remove(z); + } + } + + return hNeighbors; + } + + private static void delete(Node x, Node y, Set subset, Graph graph) { + graph.removeEdges(x, y); + + for (Node aSubset : subset) { + if (!graph.isParentOf(aSubset, x) && !graph.isParentOf(x, aSubset)) { + graph.removeEdge(x, aSubset); + graph.addDirectedEdge(x, aSubset); + } + graph.removeEdge(y, aSubset); + graph.addDirectedEdge(y, aSubset); + } + } + + private double deleteEval(Node x, Node y, SubSet h, Graph graph){ + + Set set1 = new HashSet(findNaYX(x, y, graph)); + set1.removeAll(h); + set1.addAll(graph.getParents(y)); + set1.remove(x); + return scoreGraphChangeDelete(y, x, set1); // calcular si y esta d-separado de x dado el set1 en cada grafo. + + } + + private static List findNaYX(Node x, Node y, Graph graph) { + List naYX = new LinkedList<>(graph.getAdjacentNodes(y)); + naYX.retainAll(graph.getAdjacentNodes(x)); + + for (int i = naYX.size()-1; i >= 0; i--) { + Node z = naYX.get(i); + Edge edge = graph.getEdge(y, z); + + if (!Edges.isUndirectedEdge(edge)) { + naYX.remove(z); + } + } + + return naYX; + } + + private double scoreGraphChangeDelete(Node y, Node x, Set set){ + + String key = y.getName()+x.getName()+set.toString(); + Double val = this.localScore.get(key); + if(val == null){ + double eval = 0.0; + LinkedList conditioning = new LinkedList<>(); + conditioning.addAll(set); + for(Dag g: this.initialDags){ + if(!dSeparated(g,y, x, conditioning)) return 0.0; + } + eval = 1.0; //eval / (double) this.setOfdags.size(); + val = eval; + this.localScore.put(key, val); + return eval; + }else{ + return val.doubleValue(); + } + } + + boolean dSeparated(Dag g, Node x, Node y, LinkedList cond){ + + LinkedList open = new LinkedList(); + HashMap close = new HashMap(); + open.add(x); + open.add(y); + open.addAll(cond); + while (open.size() != 0){ + Node a = open.getFirst(); + open.remove(a); + close.put(a.toString(),a); + List pa =g.getParents(a); + for(Node p : pa){ + if(close.get(p.toString()) == null){ + if(!open.contains(p)) open.addLast(p); + } + } + } + + Graph aux = new EdgeListGraph(); + + for (Node node : g.getNodes()) aux.addNode(node); + Node nodeT, nodeH; + for (Edge e : g.getEdges()){ + if(!e.isDirected()) continue; + nodeT = e.getNode1(); + nodeH = e.getNode2(); + if((close.get(nodeH.toString())!=null)&&(close.get(nodeT.toString())!=null)){ + Edge newEdge = new Edge(e.getNode1(),e.getNode2(),e.getEndpoint1(),e.getEndpoint2()); + aux.addEdge(newEdge); + } + } + + close = new HashMap(); + for(Edge e: aux.getEdges()){ + if(e.isDirected()){ + Node h; + if(e.getEndpoint1()==Endpoint.ARROW){ + h = e.getNode1(); + }else h = e.getNode2(); + if(close.get(h.toString())==null){ + close.put(h.toString(),h); + List pa = aux.getParents(h); + if(pa.size()>1){ + for(int i = 0 ; i< pa.size() - 1; i++) + for(int j = i+1; j < pa.size(); j++){ + Node p1 = pa.get(i); + Node p2 = pa.get(j); + boolean found = false; + for(Edge edge : aux.getEdges()){ + if(edge.getNode1().equals(p1)&&(edge.getNode2().equals(p2))){ + found = true; + break; + } + if(edge.getNode2().equals(p1)&&(edge.getNode1().equals(p2))){ + found = true; + break; + } + } + if(!found) aux.addUndirectedEdge(p1, p2); + } + } + + } + } + } + + for(Edge e: aux.getEdges()){ + if(e.isDirected()){ + e.setEndpoint1(Endpoint.TAIL); + e.setEndpoint2(Endpoint.TAIL); + } + } + + aux.removeNodes(cond); + + open = new LinkedList(); + close = new HashMap(); + open.add(x); + while (open.size() != 0){ + Node a = open.getFirst(); + if(a.equals(y)) return false; + open.remove(a); + close.put(a.toString(),a); + List pa =aux.getAdjacentNodes(a); + for(Node p : pa){ + if(close.get(p.toString()) == null){ + if(!open.contains(p)) open.addLast(p); + } + } + } + + return true; + } + + + public Dag getFusion(){ + + return this.outputDag; + } + + public List getOrderFusion(){ + return this.getFusion().paths().getValidOrder(this.getFusion().getNodes(),true); + } + + + private static boolean isClique(List set, Graph graph) { + List setv = new LinkedList(set); + for (int i = 0; i < setv.size() - 1; i++) { + for (int j = i + 1; j < setv.size(); j++) { + if (!graph.isAdjacentTo(setv.get(i), setv.get(j))) { + return false; + } + } + } + return true; + } + + + +} diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java index 9514eb2..33b5d9d 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java @@ -18,7 +18,6 @@ import edu.cmu.tetrad.search.utils.MeekRules; import edu.cmu.tetrad.search.utils.GraphSearchUtils; import static es.uclm.i3a.simd.consensusBN.Utils.pdagToDag; -//import experimentosFusion.RandomBN; @@ -26,27 +25,32 @@ public class ConsensusBES implements Runnable { ArrayList alpha = null; Dag outputDag = null; - AlphaOrder heuristic = null; - TransformDags imaps2alpha = null; + //AlphaOrder heuristic = null; + //TransformDags imaps2alpha = null; + ConsensusUnion consensusUnion; ArrayList setOfdags = null; ArrayList setOfOutDags = null; Dag union = null; int numberOfInsertedEdges = 0; - Map localScore = new HashMap(); + Map localScore = new HashMap<>(); public ConsensusBES(ArrayList dags){ this.setOfdags = dags; + this.consensusUnion = new ConsensusUnion(this.setOfdags); + + /* this.heuristic = new AlphaOrder(this.setOfdags); - this.heuristic.computeAlphaH2(); - this.alpha = this.heuristic.alpha; + this.heuristic.computeAlpha(); + this.alpha = this.heuristic.getOrder(); this.imaps2alpha = new TransformDags(this.setOfdags,this.alpha); this.imaps2alpha.transform(); this.numberOfInsertedEdges = imaps2alpha.getNumberOfInsertedEdges(); - this.setOfOutDags = imaps2alpha.setOfOutputDags; + this.setOfOutDags = imaps2alpha.getSetOfOutputDags(); + */ } @@ -54,11 +58,14 @@ public int getNumberOfInsertedEdges(){ return this.numberOfInsertedEdges; } - private void consensusUnion(){ - + public void consensusUnion(){ + this.union = this.consensusUnion.union(); + this.setOfOutDags = this.consensusUnion.getTransformedDags(); + + /* this.union = new Dag(this.alpha); for(Node nodei: this.alpha){ - for(Dag d : this.imaps2alpha.setOfOutputDags){ + for(Dag d : this.imaps2alpha.getSetOfOutputDags()){ Listparent = d.getParents(nodei); for(Node pa: parent){ if(!this.union.isParentOf(pa, nodei)){ @@ -68,6 +75,7 @@ private void consensusUnion(){ } } + */ // for(Edge e: this.union.getEdges()){ // for(Dag d : this.imaps2alpha.setOfOutputDags){ // if((d.getEdge(e.getNode1(), e.getNode2())==null) && (d.getEdge(e.getNode2(), e.getNode1())==null)) @@ -77,8 +85,19 @@ private void consensusUnion(){ // } } + + public Dag getUnion() { + return this.union; + } // private methods for searching + public void fusion2(){ + // 1. Apply ConsensusUnion to the set of dags + consensusUnion(); + // 2. Apply Backward Equivalence Search with D-separation + BackwardEquivalenceSearchDSep bes = new BackwardEquivalenceSearchDSep(this.union, this.setOfdags, this.setOfOutDags); + this.outputDag = bes.applyBackwardEliminationWithDSeparation(); + } public void fusion(){ @@ -99,7 +118,7 @@ public void fusion(){ //SearchGraphUtils.dagToPdag(graph); rebuildPattern(graph); Node x, y; - Set t = new HashSet(); + Set t = new HashSet<>(); do { x = y = null; Set edges1 = graph.getEdges(); diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java new file mode 100644 index 0000000..bfcdca2 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java @@ -0,0 +1,124 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Edge; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +public class ConsensusBESTest { + private ArrayList inputDags; + private ArrayList alpha; + + @BeforeEach + public void setUp() { + inputDags = new ArrayList<>(); + + // We use 4 nodes for the DAGs + Node nodeA = new GraphNode("A"); + Node nodeB = new GraphNode("B"); + Node nodeC = new GraphNode("C"); + Node nodeD = new GraphNode("D"); + + // Create first DAG with these edges: A -> B, A -> C, B -> D, C -> D + Dag dag1 = new Dag(); + dag1.addNode(nodeA); + dag1.addNode(nodeB); + dag1.addNode(nodeC); + dag1.addNode(nodeD); + + // Adding directed edges to the DAG + dag1.addDirectedEdge(nodeA, nodeB); + dag1.addDirectedEdge(nodeA, nodeC); + dag1.addDirectedEdge(nodeB, nodeD); + dag1.addDirectedEdge(nodeC, nodeD); + + // Adding the DAG to the list + inputDags.add(dag1); + + // Create second DAG with these edges: D -> C, D -> B, C -> A, B -> A + Dag dag2 = new Dag(); + dag2.addNode(nodeA); + dag2.addNode(nodeB); + dag2.addNode(nodeC); + dag2.addNode(nodeD); + + // Adding directed edges to the second DAG + dag2.addDirectedEdge(nodeD, nodeC); + dag2.addDirectedEdge(nodeD, nodeB); + dag2.addDirectedEdge(nodeC, nodeA); + dag2.addDirectedEdge(nodeB, nodeA); + + // Adding the second DAG to the list + inputDags.add(dag2); + + // Apply AlphaOrder algorithm to these dags: + AlphaOrder alphaOrder = new AlphaOrder(inputDags); + alphaOrder.computeAlpha(); + alpha = alphaOrder.getOrder(); + + } + + @Test + public void testConsensusUnionConsistency() { + ConsensusUnion cu = new ConsensusUnion(inputDags, alpha); + Dag expected = cu.union(); + assertNotNull(cu); + assertNotNull(expected); + + ConsensusBES consensusBES = new ConsensusBES(inputDags); + consensusBES.consensusUnion(); + + // Check if the union DAG is not null and has nodes + Dag unionDag = consensusBES.getUnion(); + assertNotNull(unionDag); + assertNotNull(unionDag.getNodes()); + + // Check that the union DAG is equal to the expected DAG + assertNotNull(unionDag.getNodes()); + assertNotNull(expected.getNodes()); + assertEquals(expected, unionDag); + assertEquals(expected.getNodes().size(), unionDag.getNodes().size()); + assertEquals(expected.getEdges().size(), unionDag.getEdges().size()); + + for (Node node : expected.getNodes()) { + assert unionDag.getNodes().contains(node); + } + for(Edge edge : expected.getEdges()) { + assert unionDag.getEdges().contains(edge); + } + } + + @Test + public void testConsensusBESConsistency() { + ConsensusBES consensusBES1 = new ConsensusBES(inputDags); + consensusBES1.fusion(); + Dag outputDag1 = consensusBES1.getFusion(); + assertNotNull(outputDag1); + + ConsensusBES consensusBES2 = new ConsensusBES(inputDags); + consensusBES2.fusion2(); + Dag outputDag2 = consensusBES2.getFusion(); + assertNotNull(outputDag2); + + // Check that both outputs are the same + assertNotNull(outputDag1); + assertNotNull(outputDag2); + assertEquals(outputDag1, outputDag2); + assertEquals(outputDag1.getNodes().size(), outputDag2.getNodes().size()); + assertEquals(outputDag1.getEdges().size(), outputDag2.getEdges().size()); + + for (Node node : outputDag1.getNodes()) { + assert outputDag2.getNodes().contains(node); + } + for(Edge edge : outputDag1.getEdges()) { + assert outputDag2.getEdges().contains(edge); + } + } +} From d5f2f58ed10b258ee7843cce7817cbfb1c20b45e Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Wed, 16 Jul 2025 13:05:17 +0200 Subject: [PATCH 07/32] Cleaning and testing ConsensusBES --- .../BackwardEquivalenceSearchDSep.java | 36 +- .../i3a/simd/consensusBN/ConsensusBES.java | 566 ++++-------------- .../i3a/simd/consensusBN/ConsensusUnion.java | 8 + .../BackwardEquivalenceSearchDSepTest.java | 112 ++++ .../simd/consensusBN/ConsensusBESTest.java | 111 +++- 5 files changed, 346 insertions(+), 487 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index 079ce49..d30b810 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -329,6 +329,7 @@ boolean dSeparated(Dag g, Node x, Node y, LinkedList cond){ close.put(a.toString(),a); List pa =aux.getAdjacentNodes(a); for(Node p : pa){ + if(p == null) continue; if(close.get(p.toString()) == null){ if(!open.contains(p)) open.addLast(p); } @@ -338,28 +339,23 @@ boolean dSeparated(Dag g, Node x, Node y, LinkedList cond){ return true; } - - public Dag getFusion(){ - - return this.outputDag; - } - - public List getOrderFusion(){ - return this.getFusion().paths().getValidOrder(this.getFusion().getNodes(),true); - } - private static boolean isClique(List set, Graph graph) { - List setv = new LinkedList(set); - for (int i = 0; i < setv.size() - 1; i++) { - for (int j = i + 1; j < setv.size(); j++) { - if (!graph.isAdjacentTo(setv.get(i), setv.get(j))) { - return false; - } - } - } - return true; - } + private static boolean isClique(List set, Graph graph) { + List setv = new LinkedList(set); + for (int i = 0; i < setv.size() - 1; i++) { + for (int j = i + 1; j < setv.size(); j++) { + if (!graph.isAdjacentTo(setv.get(i), setv.get(j))) { + return false; + } + } + } + return true; + } + + public int getNumberOfInsertedEdges() { + return this.numberOfInsertedEdges; + } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java index 33b5d9d..d8514ef 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java @@ -2,486 +2,164 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; import java.util.Map; -import java.util.Set; import edu.cmu.tetrad.graph.Dag; -import edu.cmu.tetrad.graph.Edge; -import edu.cmu.tetrad.graph.EdgeListGraph; -import edu.cmu.tetrad.graph.Edges; -import edu.cmu.tetrad.graph.Endpoint; -import edu.cmu.tetrad.graph.Graph; import edu.cmu.tetrad.graph.Node; -import edu.cmu.tetrad.search.utils.MeekRules; -import edu.cmu.tetrad.search.utils.GraphSearchUtils; -import static es.uclm.i3a.simd.consensusBN.Utils.pdagToDag; - - +/** + * This class implements the Optimal Fusion GES^h_d algorithm, which applies a Consensus Union followed by a Backward Equivalence Search (BES) with D-separation. + * The algorithm first computes a consensus DAG from a set of input DAGs using the ConsensusUnion class. + * After obtaining the consensus DAG, it applies the Backward Equivalence Search with D-separation to refine the graph, achieving the optimal fusion BN. + * The resulting output DAG is stored in the outputDag attribute. + */ public class ConsensusBES implements Runnable { - ArrayList alpha = null; - Dag outputDag = null; - //AlphaOrder heuristic = null; - //TransformDags imaps2alpha = null; - ConsensusUnion consensusUnion; - ArrayList setOfdags = null; - ArrayList setOfOutDags = null; - Dag union = null; + /** + * Final output DAG after applying the Consensus Union and Backward Equivalence Search with D-separation. + * This DAG represents the optimal fusion of the input DAGs. + * It is computed by first merging the input DAGs into a consensus DAG and then refining it using the BES with D-separation. + * + * @see ConsensusUnion + * @see BackwardEquivalenceSearchDSepTest + */ + private Dag outputDag; + + /** + * Instance of ConsensusUnion used to compute the consensus DAG from the input DAGs. + * This instance is initialized with the set of input DAGs and computes the alpha order of nodes using AlphaOrder heuristic (Greedy Heuristic Order). + * + * @see ConsensusUnion + * @see AlphaOrder + */ + private final ConsensusUnion consensusUnion; + + /** + * List of input DAGs to be fused using the ConsensusBES algorithm. + */ + private final ArrayList inputDags; + + /** + * List of transformed DAGs after applying the alpha order to the input DAGs. + * @see BetaToAlpha + * @see TransformDags + */ + private ArrayList transformedDags; + + /** + * Resulting DAG afther applying the Consensus Union algorithm. + * This DAG contains the union of all edges from the transformed input DAGs, ensuring that the resulting graph is acyclic. + * The number of edges inserted during the union process can be retrieved using getNumberOfInsertedEdges. + */ + private Dag union = null; + + /** + * Number of edges inserted during the consensus union process and the Backward Equivalence Search process. + */ int numberOfInsertedEdges = 0; - Map localScore = new HashMap<>(); - - + /** + * Local score map used to store the scores of graph changes during the Backward Equivalence Search. + * The key is a string representation of the nodes and their conditioning set, and the value is the score associated with that configuration. + */ + private final Map localScore = new HashMap<>(); + + /** + * Constructor for ConsensusBES that initializes the union process with a list of DAGs. + * It creates an instance of ConsensusUnion to compute the consensus DAG. + * @param dags the list of input DAGs to be merged. + */ public ConsensusBES(ArrayList dags){ - this.setOfdags = dags; - this.consensusUnion = new ConsensusUnion(this.setOfdags); - - /* - this.heuristic = new AlphaOrder(this.setOfdags); - - this.heuristic.computeAlpha(); - this.alpha = this.heuristic.getOrder(); - this.imaps2alpha = new TransformDags(this.setOfdags,this.alpha); - - this.imaps2alpha.transform(); - this.numberOfInsertedEdges = imaps2alpha.getNumberOfInsertedEdges(); - this.setOfOutDags = imaps2alpha.getSetOfOutputDags(); - */ - } - - - public int getNumberOfInsertedEdges(){ - return this.numberOfInsertedEdges; + this.inputDags = dags; + this.consensusUnion = new ConsensusUnion(this.inputDags); } + /** + * Performs the consensus union operation by calling the union method of the ConsensusUnion instance. + * This method initializes the union process, transforming the input DAGs based on the alpha order and merging them into a single consensus DAG. + * After the union, it retrieves the transformed DAGs and updates the number of inserted edges. + */ public void consensusUnion(){ this.union = this.consensusUnion.union(); - this.setOfOutDags = this.consensusUnion.getTransformedDags(); - - /* - this.union = new Dag(this.alpha); - for(Node nodei: this.alpha){ - for(Dag d : this.imaps2alpha.getSetOfOutputDags()){ - Listparent = d.getParents(nodei); - for(Node pa: parent){ - if(!this.union.isParentOf(pa, nodei)){ - this.union.addEdge(new Edge(pa,nodei,Endpoint.TAIL,Endpoint.ARROW)); - } - } - } - - } - */ -// for(Edge e: this.union.getEdges()){ -// for(Dag d : this.imaps2alpha.setOfOutputDags){ -// if((d.getEdge(e.getNode1(), e.getNode2())==null) && (d.getEdge(e.getNode2(), e.getNode1())==null)) -// this.numberOfInsertedEdges++; -// -// } -// } - - } - - public Dag getUnion() { - return this.union; + this.transformedDags = this.consensusUnion.getTransformedDags(); + this.numberOfInsertedEdges += consensusUnion.getNumberOfInsertedEdges(); } - // private methods for searching - public void fusion2(){ + /** + * Applies the fusion process by first performing the consensus union and then applying the Backward Equivalence Search with D-separation. + * This method modifies the outputDag attribute to contain the final fused DAG after applying both steps. + */ + public void fusion(){ // 1. Apply ConsensusUnion to the set of dags consensusUnion(); // 2. Apply Backward Equivalence Search with D-separation - BackwardEquivalenceSearchDSep bes = new BackwardEquivalenceSearchDSep(this.union, this.setOfdags, this.setOfOutDags); + BackwardEquivalenceSearchDSep bes = new BackwardEquivalenceSearchDSep(this.union, this.inputDags, this.transformedDags); this.outputDag = bes.applyBackwardEliminationWithDSeparation(); + // 3. Updating numberOfInsertedEdges + this.numberOfInsertedEdges += bes.getNumberOfInsertedEdges(); } - - public void fusion(){ - - // System.out.println("\n** BACKWARD ELIMINATION SEARCH (BES)"); - //PowerSetFabric.setMode(PowerSetFabric.MODE_BES); - double score = 0; - double bestScore = score; - Graph graph = null; - - consensusUnion(); - - graph = new EdgeListGraph(new LinkedList<>(this.union.getNodes())); - for(Edge e: this.union.getEdges()){ - graph.addEdge(e); - } - - //SearchGraphUtils.dagToPdag(graph); - rebuildPattern(graph); - Node x, y; - Set t = new HashSet<>(); - do { - x = y = null; - Set edges1 = graph.getEdges(); - List edges = new ArrayList(); - - for (Edge edge : edges1) { - Node _x = edge.getNode1(); - Node _y = edge.getNode2(); - - if (Edges.isUndirectedEdge(edge)) { - edges.add(Edges.directedEdge(_x, _y)); - edges.add(Edges.directedEdge(_y, _x)); - } else { - edges.add(edge); - } - } - for (Edge edge : edges) { - - Node _x = Edges.getDirectedEdgeTail(edge); - Node _y = Edges.getDirectedEdgeHead(edge); - - List hNeighbors = getHNeighbors(_x, _y, graph); -// List> hSubsets = powerSet(hNeighbors); - PowerSet hSubsets= PowerSetFabric.getPowerSet(_x,_y,hNeighbors); - - while(hSubsets.hasMoreElements()) { - SubSet hSubset=hSubsets.nextElement(); - double deleteEval = deleteEval(_x, _y, hSubset, graph); - if (!(deleteEval >= 1.0)) deleteEval = 0.0; - double evalScore = score + deleteEval; - - //System.out.println("Attempt removing " + _x + "-->" + _y + "(" +evalScore + ") "+ hSubset.toString()); - - if (!(evalScore > bestScore)) { - continue; - } - - // INICIO TEST 1 - List naYXH = findNaYX(_x, _y, graph); - naYXH.removeAll(hSubset); - if (!isClique(naYXH, graph)) { -// hSubsets.firstTest(true); // Si pasa para H entonces pasa para cualquier H' | H' contiene H - continue; - } - // FIN TEST 1 - - bestScore = evalScore; - x = _x; - y = _y; - t = hSubset; - } - - } - if (x != null) { - System.out.println(" "); - System.out.println("DELETE " + graph.getEdge(x, y) + t.toString() + " (" +bestScore + ")"); - System.out.println(" "); - delete(x, y, t, graph); - rebuildPattern(graph); - int deletedEdges = 0; - for(int g = 0; g subset, Graph graph) { - graph.removeEdges(x, y); - - for (Node aSubset : subset) { - if (!graph.isParentOf(aSubset, x) && !graph.isParentOf(x, aSubset)) { - graph.removeEdge(x, aSubset); - graph.addDirectedEdge(x, aSubset); - } - graph.removeEdge(y, aSubset); - graph.addDirectedEdge(y, aSubset); - } - } - - - private void rebuildPattern(Graph graph) { - GraphSearchUtils.basicCpdag(graph); - pdag(graph); - } - - /** - * Fully direct a graph with background knowledge. I am not sure how to - * adapt Chickering's suggested algorithm above (dagToPdag) to incorporate - * background knowledge, so I am also implementing this algorithm based on - * Meek's 1995 UAI paper. Notice it is the same implemented in PcSearch. - *

*IMPORTANT!* *It assumes all colliders are oriented, as well as - * arrows dictated by time order.* - * - * ELIMINADO BACKGROUND KNOWLEDGE - */ - private void pdag(Graph graph) { - MeekRules rules = new MeekRules(); - rules.setMeekPreventCycles(true); - rules.orientImplied(graph); - } - - - private static boolean isClique(List set, Graph graph) { - List setv = new LinkedList(set); - for (int i = 0; i < setv.size() - 1; i++) { - for (int j = i + 1; j < setv.size(); j++) { - if (!graph.isAdjacentTo(setv.get(i), setv.get(j))) { - return false; - } - } - } - return true; - } - - private static List getHNeighbors(Node x, Node y, Graph graph) { - List hNeighbors = new LinkedList(graph.getAdjacentNodes(y)); - hNeighbors.retainAll(graph.getAdjacentNodes(x)); - - for (int i = hNeighbors.size() - 1; i >= 0; i--) { - Node z = hNeighbors.get(i); - Edge edge = graph.getEdge(y, z); - if (!Edges.isUndirectedEdge(edge)) { - hNeighbors.remove(z); - } - } - - return hNeighbors; - } - - - double deleteEval(Node x, Node y, SubSet h, Graph graph){ - - Set set1 = new HashSet(findNaYX(x, y, graph)); - set1.removeAll(h); - set1.addAll(graph.getParents(y)); - set1.remove(x); - return scoreGraphChangeDelete(y, x, set1); // calcular si y esta d-separado de x dado el set1 en cada grafo. - - } - - double scoreGraphChangeDelete(Node y, Node x, Set set){ - - String key = y.getName()+x.getName()+set.toString(); - Double val = this.localScore.get(key); - if(val == null){ - double eval = 0.0; - LinkedList conditioning = new LinkedList(); - conditioning.addAll(set); - for(Dag g: this.setOfdags){ - if(!dSeparated(g,y, x, conditioning)) return 0.0; - } - eval = 1.0; //eval / (double) this.setOfdags.size(); - val = eval; - this.localScore.put(key, val); - return eval; - }else{ - return val.doubleValue(); - } - } - - - boolean dSeparated(Dag g, Node x, Node y, LinkedList cond){ - - LinkedList open = new LinkedList(); - HashMap close = new HashMap(); - open.add(x); - open.add(y); - open.addAll(cond); - while (open.size() != 0){ - Node a = open.getFirst(); - open.remove(a); - close.put(a.toString(),a); - List pa =g.getParents(a); - for(Node p : pa){ - if(close.get(p.toString()) == null){ - if(!open.contains(p)) open.addLast(p); - } - } - } - - Graph aux = new EdgeListGraph(); - - for (Node node : g.getNodes()) aux.addNode(node); - Node nodeT, nodeH; - for (Edge e : g.getEdges()){ - if(!e.isDirected()) continue; - nodeT = e.getNode1(); - nodeH = e.getNode2(); - if((close.get(nodeH.toString())!=null)&&(close.get(nodeT.toString())!=null)){ - Edge newEdge = new Edge(e.getNode1(),e.getNode2(),e.getEndpoint1(),e.getEndpoint2()); - aux.addEdge(newEdge); - } - } - - close = new HashMap(); - for(Edge e: aux.getEdges()){ - if(e.isDirected()){ - Node h; - if(e.getEndpoint1()==Endpoint.ARROW){ - h = e.getNode1(); - }else h = e.getNode2(); - if(close.get(h.toString())==null){ - close.put(h.toString(),h); - List pa = aux.getParents(h); - if(pa.size()>1){ - for(int i = 0 ; i< pa.size() - 1; i++) - for(int j = i+1; j < pa.size(); j++){ - Node p1 = pa.get(i); - Node p2 = pa.get(j); - boolean found = false; - for(Edge edge : aux.getEdges()){ - if(edge.getNode1().equals(p1)&&(edge.getNode2().equals(p2))){ - found = true; - break; - } - if(edge.getNode2().equals(p1)&&(edge.getNode1().equals(p2))){ - found = true; - break; - } - } - if(!found) aux.addUndirectedEdge(p1, p2); - } - } - - } - } - } - - for(Edge e: aux.getEdges()){ - if(e.isDirected()){ - e.setEndpoint1(Endpoint.TAIL); - e.setEndpoint2(Endpoint.TAIL); - } - } - - aux.removeNodes(cond); - - open = new LinkedList(); - close = new HashMap(); - open.add(x); - while (open.size() != 0){ - Node a = open.getFirst(); - if(a.equals(y)) return false; - open.remove(a); - close.put(a.toString(),a); - List pa =aux.getAdjacentNodes(a); - for(Node p : pa){ - if(close.get(p.toString()) == null){ - if(!open.contains(p)) open.addLast(p); - } - } - } - - return true; - } - - - - - private static List findNaYX(Node x, Node y, Graph graph) { - List naYX = new LinkedList(graph.getAdjacentNodes(y)); - naYX.retainAll(graph.getAdjacentNodes(x)); - - for (int i = naYX.size()-1; i >= 0; i--) { - Node z = naYX.get(i); - Edge edge = graph.getEdge(y, z); - - if (!Edges.isUndirectedEdge(edge)) { - naYX.remove(z); - } - } - - return naYX; - } - + /** + * Returns the output DAG after applying the Consensus Union and Backward Equivalence Search with D-separation. + * This method retrieves the final fused DAG, which represents the optimal fusion of the input DAGs. + * @return the resulting output DAG after the fusion process. + */ public Dag getFusion(){ - return this.outputDag; } + /** + * Returns a valid ancestral order of the nodes in the fused DAG. + * @return + */ public List getOrderFusion(){ return this.getFusion().paths().getValidOrder(this.getFusion().getNodes(),true); } - - public static void main(String args[]) { - - - System.out.println("Grafos de Partida: "); - - // (seed, n. variables, n egdes max, n.dags, mutation(n. de operaciones)) - RandomBN setOfBNs = new RandomBN(0, Integer.parseInt(args[0]), Integer.parseInt(args[1]), - Integer.parseInt(args[2]), Integer.parseInt(args[3])); - setOfBNs.setMaxInDegree(4); - setOfBNs.setMaxOutDegree(4); - setOfBNs.generate(); + /** + * Returns the number of edges inserted during the consensus union and removed in the Backward Equivalence Search with D-separation. + * @return + */ + public int getNumberOfInsertedEdges(){ + return this.numberOfInsertedEdges; + } - for (int i = 0; i < setOfBNs.setOfRandomBNs.size(); i++) { - System.out.println("red de partida: " + i); - System.out.println("---------------------"); - System.out.println("Grafo: "); - System.out.println(setOfBNs.setOfRandomDags.get(i).toString()); -// System.out.println("Probabilidades: "); -// System.out.println(setOfBNs.setOfRandomBNs.get(i).toString()); -// System.out.println("_____________________"); -// System.out.println("Datos Simulados"); -// System.out.println(setOfBNs.setOfSampledBNs.get(i).toString()); + /** + * Returns the union DAG resulting from the consensus union process. + * @return the union DAG after merging the transformed input DAGs. + */ + public Dag getUnion() { + return this.union; + } -// -// } -// // - ConsensusBES conDag= null; -// - conDag = new ConsensusBES(setOfBNs.setOfRandomDags); - conDag.fusion(); - Dag g = conDag.getFusion(); - System.out.println("grafo consenso: "+ g +" Complejidad de la Fusion: "+ conDag.getNumberOfInsertedEdges() - + " "+ conDag.union.getNumEdges()); - System.out.println("Orden Inicial Heu: "+conDag.alpha.toString()); - System.out.println("Orden de consenso: "+conDag.getOrderFusion().toString()); -// -//// HierarchicalAgglomerativeClustererBNs Cfusion = new HierarchicalAgglomerativeClustererBNs(setOfBNs.setOfRandomDags,0.50); -//// int l = Cfusion.cluster(); -//// System.out.println("Nivel de Fusion: "+l); -//// System.out.println(Cfusion.computeConsensusDag(l).toString()); -// } -// + /** + * Returns the ConsensusUnion instance used in this ConsensusBES. + * This instance contains the logic for merging the input DAGs and computing the alpha order. + * @return the ConsensusUnion instance associated with this ConsensusBES. + */ + public ConsensusUnion getConsensusUnion() { + return this.consensusUnion; + } + + /** + * Returns the list of transformed DAGs after applying the alpha order to the input DAGs. + * This method retrieves the transformed DAGs that were used in the consensus union process. + * @return the list of transformed DAGs. + */ + public ArrayList getTransformedDags() { + if (this.transformedDags != null) { + return this.transformedDags; + } else { + throw new IllegalStateException("Transformed DAGs have not been initialized. Please call fusion() first."); } } - + + /** + * Runs the ConsensusBES algorithm in a thread, performing the consensus union and the Backward Equivalence Search with D-separation. + */ @Override public void run() { - + this.fusion(); } } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java index ba63e1b..c0dd4ae 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java @@ -162,6 +162,14 @@ void setDags(ArrayList dags){ public void run() { this.union = this.union(); } + + public ArrayList getTransformedDags() { + if (this.imaps2alpha != null) { + return this.imaps2alpha.getSetOfOutputDags(); + } else { + throw new IllegalStateException("TransformDags has not been initialized. Please call union() first."); + } + } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java new file mode 100644 index 0000000..7078928 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java @@ -0,0 +1,112 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Edge; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.GraphUtils; +import edu.cmu.tetrad.graph.Node; + +class BackwardEquivalenceSearchDSepTest { + + private Dag createSimpleDag() { + // A -> B -> C + Node a = new GraphNode("A"); + Node b = new GraphNode("B"); + Node c = new GraphNode("C"); + + Dag dag = new Dag(); + dag.addNode(a); + dag.addNode(b); + dag.addNode(c); + + dag.addDirectedEdge(a, b); + dag.addDirectedEdge(b, c); + + return dag; + } + + private ArrayList createDagList(int copies) { + ArrayList list = new ArrayList<>(); + for (int i = 0; i < copies; i++) { + list.add(createSimpleDag()); + } + return list; + } + + @Test + void testApplyBESdDoesNotThrow() { + Dag unionDag = createSimpleDag(); + ArrayList initialDags = createDagList(3); + ArrayList transformedDags = createDagList(3); + + BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, transformedDags); + + assertDoesNotThrow(() -> { + Dag output = besd.applyBackwardEliminationWithDSeparation(); + assertNotNull(output); + }); + } + + @Test + void testOutputIsDAG() { + Dag unionDag = createSimpleDag(); + ArrayList initialDags = createDagList(2); + ArrayList transformedDags = createDagList(2); + + BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, transformedDags); + Dag outputDag = besd.applyBackwardEliminationWithDSeparation(); + + assertTrue(GraphUtils.isDag(outputDag), "El resultado no es un DAG válido."); + } + + @Test + void testAristasSePuedenEliminar() { + // Creamos un grafo donde A -> B, pero en todos los DAGs está A B (no conectados) + Node a = new GraphNode("A"); + Node b = new GraphNode("B"); + + Dag unionDag = new Dag(); + unionDag.addNode(a); + unionDag.addNode(b); + unionDag.addDirectedEdge(a, b); + + // DAGs originales sin esa arista + Dag dag1 = new Dag(); + dag1.addNode(a); + dag1.addNode(b); + // sin conexión + + ArrayList initialDags = new ArrayList<>(); + initialDags.add(dag1); + ArrayList transformedDags = new ArrayList<>(); + transformedDags.add(dag1); + + BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, transformedDags); + Dag outputDag = besd.applyBackwardEliminationWithDSeparation(); + + // Debe eliminar la arista A -> B por no tener soporte + Edge deletedEdge = outputDag.getEdge(a, b); + assertNull(deletedEdge, "La arista A -> B debería haberse eliminado."); + } + + @Test + void testGetNumberOfInsertedEdgesReflectsChanges() { + Dag unionDag = createSimpleDag(); + ArrayList dags = createDagList(2); + + BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, dags, dags); + besd.applyBackwardEliminationWithDSeparation(); + + int insertedEdges = besd.getNumberOfInsertedEdges(); + // En el peor de los casos no ha eliminado ninguna, pero nunca debe ser negativo + assertTrue(insertedEdges >= 0, "El número de aristas insertadas no puede ser negativo."); + } +} diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java index bfcdca2..da9a321 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java @@ -1,9 +1,14 @@ package es.uclm.i3a.simd.consensusBN; import java.util.ArrayList; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -96,29 +101,89 @@ public void testConsensusUnionConsistency() { } @Test - public void testConsensusBESConsistency() { - ConsensusBES consensusBES1 = new ConsensusBES(inputDags); - consensusBES1.fusion(); - Dag outputDag1 = consensusBES1.getFusion(); - assertNotNull(outputDag1); - - ConsensusBES consensusBES2 = new ConsensusBES(inputDags); - consensusBES2.fusion2(); - Dag outputDag2 = consensusBES2.getFusion(); - assertNotNull(outputDag2); - - // Check that both outputs are the same - assertNotNull(outputDag1); - assertNotNull(outputDag2); - assertEquals(outputDag1, outputDag2); - assertEquals(outputDag1.getNodes().size(), outputDag2.getNodes().size()); - assertEquals(outputDag1.getEdges().size(), outputDag2.getEdges().size()); - - for (Node node : outputDag1.getNodes()) { - assert outputDag2.getNodes().contains(node); - } - for(Edge edge : outputDag1.getEdges()) { - assert outputDag2.getEdges().contains(edge); + public void testRandomBNFusion(){ + // (seed, n. variables, n egdes max, n.dags, mutation(n. de operaciones)) + RandomBN setOfDags = new RandomBN(0, 20, 50, + 4,3); + setOfDags.setMaxInDegree(4); + setOfDags.setMaxOutDegree(4); + setOfDags.generate(); + + ConsensusBES conDag = new ConsensusBES(setOfDags.setOfRandomDags); + conDag.fusion(); + Dag besDag = conDag.getFusion(); + Dag unionDag = conDag.getUnion(); + ConsensusUnion consensusUnion = conDag.getConsensusUnion(); + int totalNumberOfInsertedEdges = conDag.getNumberOfInsertedEdges(); + int consensusNumberOfInsertedEdges = consensusUnion.getNumberOfInsertedEdges(); + + assertNotNull(besDag); + assertNotNull(unionDag); + assertNotNull(consensusUnion); + assertEquals(besDag.getNodes().size(), unionDag.getNodes().size()); + assert consensusNumberOfInsertedEdges >= 0; + assert consensusNumberOfInsertedEdges >= totalNumberOfInsertedEdges; + } + + + @Test + void testFusionProducesDag() { + ConsensusBES fusionAlgorithm = new ConsensusBES(inputDags); + fusionAlgorithm.fusion(); + + Dag result = fusionAlgorithm.getFusion(); + assertNotNull(result, "El DAG de salida no debe ser null."); + assertFalse(result.paths().existsDirectedCycle(), "El DAG resultante no debe tener ciclos."); + } + + @Test + void testEdgeInsertionCountIsCorrectlyComputed() { + ConsensusBES fusionAlgorithm = new ConsensusBES(inputDags); + fusionAlgorithm.fusion(); + + int insertedEdges = fusionAlgorithm.getNumberOfInsertedEdges(); + assertTrue(insertedEdges >= 0, "El número de aristas insertadas debe ser >= 0."); + } + + @Test + void testFusionOrderIsValid() { + ConsensusBES fusionAlgorithm = new ConsensusBES(inputDags); + fusionAlgorithm.fusion(); + + List order = fusionAlgorithm.getOrderFusion(); + assertNotNull(order, "El orden de fusión no debe ser null."); + assertEquals(4, order.size(), "El orden de fusión debe tener 3 nodos."); + } + + @Test + void testTransformedDagsAreAccessibleAfterFusion() { + ConsensusBES fusionAlgorithm = new ConsensusBES(inputDags); + fusionAlgorithm.fusion(); + + ArrayList transformed = fusionAlgorithm.getTransformedDags(); + assertEquals(2, transformed.size(), "Debe haber 2 DAGs transformados."); + } + + @Test + void testGetTransformedDagsWithoutFusionThrowsException() { + ConsensusBES fusionAlgorithm = new ConsensusBES(inputDags); + + assertThrows(IllegalStateException.class, fusionAlgorithm::getTransformedDags, + "Debe lanzar una excepción si se accede a los DAGs transformados sin llamar a fusion()."); + } + + @Test + void testThreadExecutionWithRunMethod() { + ConsensusBES fusionAlgorithm = new ConsensusBES(inputDags); + Thread thread = new Thread(fusionAlgorithm); + thread.start(); + try { + thread.join(); + } catch (InterruptedException e) { + fail("El hilo fue interrumpido."); } + + assertNotNull(fusionAlgorithm.getFusion(), "El DAG resultante debe existir tras ejecutar run()."); } + } From 48868e83da4a366e2650c75f24b2a1b612f166f2 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:18:40 +0200 Subject: [PATCH 08/32] Cleaning applyBackwardEliminationWithDSeparation in BackwardEquivalenceSearchDSep --- .../BackwardEquivalenceSearchDSep.java | 242 ++++++++++++------ 1 file changed, 165 insertions(+), 77 deletions(-) diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index d30b810..e122886 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -39,91 +39,75 @@ public BackwardEquivalenceSearchDSep(Dag union, ArrayListinitialDags, Array } public Dag applyBackwardEliminationWithDSeparation(){ - // Implement the BESd algorithm logic here - // This is a placeholder for the actual BESd algorithm implementation - // The algorithm should modify the graph based on the BESd logic - rebuildPattern(graph); - Node x, y; - Set t = new HashSet<>(); - double score = 0; - double bestScore = score; + double score = 0; + EdgeCandidate bestCandidate; + + // Creating a pdag from the graph + rebuildPattern(graph); + + // While there are edges to delete, search for the best edge to delete do { - x = y = null; - Set edges1 = graph.getEdges(); - List edges = new ArrayList<>(); - - for (Edge edge : edges1) { - Node _x = edge.getNode1(); - Node _y = edge.getNode2(); - - if (Edges.isUndirectedEdge(edge)) { - edges.add(Edges.directedEdge(_x, _y)); - edges.add(Edges.directedEdge(_y, _x)); - } else { - edges.add(edge); - } - } - for (Edge edge : edges) { - - Node _x = Edges.getDirectedEdgeTail(edge); - Node _y = Edges.getDirectedEdgeHead(edge); + // Make sure that any undirected edge is transformed into two directed edges + List edges = cleanUndirectedEdges(); + + // Find the best edge to delete + bestCandidate = calculateBestCandidateEdge(edges, score); + /* for (Edge edge : edges) { + // Getting candidate edge to delete + Node candidateTail = Edges.getDirectedEdgeTail(edge); + Node candidateHead = Edges.getDirectedEdgeHead(edge); - List hNeighbors = getHNeighbors(_x, _y, graph); -// List> hSubsets = powerSet(hNeighbors); - PowerSet hSubsets= PowerSetFabric.getPowerSet(_x,_y,hNeighbors); + List hNeighbors = getHNeighbors(candidateTail, candidateHead, graph); + PowerSet hSubsets= PowerSetFabric.getPowerSet(candidateTail,candidateHead,hNeighbors); while(hSubsets.hasMoreElements()) { SubSet hSubset=hSubsets.nextElement(); - double deleteEval = deleteEval(_x, _y, hSubset, graph); - if (!(deleteEval >= 1.0)) deleteEval = 0.0; - double evalScore = score + deleteEval; - //System.out.println("Attempt removing " + _x + "-->" + _y + "(" +evalScore + ") "+ hSubset.toString()); - - if (!(evalScore > bestScore)) { - continue; - } - - // INICIO TEST 1 - List naYXH = findNaYX(_x, _y, graph); + // Checking if {naYXH} \ {hSubset} is a clique + List naYXH = findNaYX(candidateTail, candidateHead, graph); naYXH.removeAll(hSubset); if (!isClique(naYXH, graph)) { -// hSubsets.firstTest(true); // Si pasa para H entonces pasa para cualquier H' | H' contiene H + continue; + } + + // Calculating the score of the candidate edge deletion + double deleteEval = deleteEval(candidateTail, candidateHead, hSubset, graph); + + // Setting limit for deleteEval + if (!(deleteEval >= 1.0)) deleteEval = 0.0; + + // If the score is not better than the best score, continue + double evalScore = score + deleteEval; + if (!(evalScore > bestScore)) { continue; } - // FIN TEST 1 + // Updating variables for the best edge deletion bestScore = evalScore; - x = _x; - y = _y; - t = hSubset; + bestTail = candidateTail; + bestHead = candidateHead; + bestSetParents = hSubset; } + } */ + // + if (bestCandidate != null) { + score = executeEdgeDeletion(bestCandidate); } - if (x != null) { - System.out.println(" "); - System.out.println("DELETE " + graph.getEdge(x, y) + t.toString() + " (" +bestScore + ")"); - System.out.println(" "); - delete(x, y, t, graph); - rebuildPattern(graph); - int deletedEdges = 0; - for(int g = 0; g cleanUndirectedEdges() { + Set edges1 = graph.getEdges(); + List edges = new ArrayList<>(); + + for (Edge edge : edges1) { + Node _x = edge.getNode1(); + Node _y = edge.getNode2(); + + if (Edges.isUndirectedEdge(edge)) { + edges.add(Edges.directedEdge(_x, _y)); + edges.add(Edges.directedEdge(_y, _x)); + } else { + edges.add(edge); + } + } + return edges; + } + + private EdgeCandidate calculateBestCandidateEdge(List edges, double score){ + double bestScore = score; + EdgeCandidate bestCandidate = null; + for(Edge edge : edges){ + // Getting candidate edge to delete + Node candidateTail = Edges.getDirectedEdgeTail(edge); + Node candidateHead = Edges.getDirectedEdgeHead(edge); + + List hNeighbors = getHNeighbors(candidateTail, candidateHead, graph); + PowerSet hSubsets= PowerSetFabric.getPowerSet(candidateTail,candidateHead,hNeighbors); + + while(hSubsets.hasMoreElements()) { + // Getting a subset of hNeighbors + SubSet hSubset=hSubsets.nextElement(); + + // Checking if {naYXH} \ {hSubset} is a clique + List naYXH = findNaYX(candidateTail, candidateHead, graph); + naYXH.removeAll(hSubset); + if (!isClique(naYXH, graph)) { + continue; + } + + // Calculating the score of the candidate edge deletion + double deleteEval = deleteEval(candidateTail, candidateHead, hSubset, graph); + + // Setting limit for deleteEval + if (!(deleteEval >= 1.0)) deleteEval = 0.0; + + // If the score is not better than the best score, continue + double evalScore = score + deleteEval; + if (!(evalScore > bestScore)) { + continue; + } + + // Updating best candidate edge + bestCandidate = new EdgeCandidate(candidateTail, candidateHead, hSubset); + bestCandidate.score = evalScore; + + // Updating score for the best edge deletion + bestScore = evalScore; + } + } + return bestCandidate; + } + + private double executeEdgeDeletion(EdgeCandidate bestCandidate) { + Node bestTail; + Node bestHead; + Set bestSetParents; + double score; + double bestScore; + bestTail = bestCandidate.tail; + bestHead = bestCandidate.head; + bestSetParents = bestCandidate.conditioningSet; + bestScore = bestCandidate.score; + + // Applying delete + System.out.println(" "); + System.out.println("DELETE " + graph.getEdge(bestTail, bestHead) + bestSetParents.toString() + " (" +bestScore + ")"); + System.out.println(" "); + delete(bestTail, bestHead, bestSetParents, graph); + + // Rebuilding the pattern after deleting the edge + rebuildPattern(graph); + + // Updating the number of inserted edges + int deletedEdges = 0; + for(int g = 0; g conditioningSet; + public double score; + public EdgeCandidate(Node tail, Node head, Set conditioningSet) { + this.tail = tail; + this.head = head; + this.conditioningSet = conditioningSet; + } + } + } From 3d8b651f7874b3c9403f34a6e335896847560f2c Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Thu, 17 Jul 2025 14:56:20 +0200 Subject: [PATCH 09/32] Cleaning and testing d-separation method and BESd algorithm --- .../BackwardEquivalenceSearchDSep.java | 267 +++++++++--------- .../es/uclm/i3a/simd/consensusBN/Utils.java | 109 +++++++ .../uclm/i3a/simd/consensusBN/UtilsTest.java | 102 +++++++ 3 files changed, 348 insertions(+), 130 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index e122886..5331c50 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -1,6 +1,8 @@ package es.uclm.i3a.simd.consensusBN; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -19,16 +21,76 @@ import edu.cmu.tetrad.search.utils.MeekRules; import static es.uclm.i3a.simd.consensusBN.Utils.pdagToDag; +/** + * This class implements the Backward Equivalence Search with D-Separation + * algorithm for consensus Bayesian networks. It uses an implementation of + * second phase of the Greedy Equivalence Search (GES) algorithm, the Backward + * Equivalence Search (BES), to refine a consensus DAG by removing edges while + * ensuring that the resulting graph remains a Directed Acyclic Graph (DAG). + * Since no data is available, the algorithm relies on D-separation to + * determine whether two nodes are conditionally independent given a set of + * other nodes. For this, the algorithm uses the set of input DAGs to check + * whether the deletion of an edge maintains the d-separation condition. + */ public class BackwardEquivalenceSearchDSep { - + /** + * The graph representing the consensus DAG after applying the Backward + * Equivalence Search with D-separation. + * This graph is built from the union of the transformed input DAGs and is + * refined by removing edges based on d-separation checks. + * + * @see ConsensusUnion + * @see TransformDags + */ private final Graph graph; + + /** + * List of initial DAGs used to check how many edges are deleted. + */ private final ArrayList transformedDags; + + /** + * List of initial DAGs used to check the d-separation condition. + * This list is used to verify whether the deletion of an edge maintains the + * d-separation condition across all input DAGs. + * + * @see Utils#dSeparated(Dag, Node, Node, List) + */ private final ArrayList initialDags; + + /** + * The output DAG after applying the Backward Equivalence Search with D-separation. + * This DAG is the final result after removing edges from the consensus DAG + * while ensuring that the d-separation condition is maintained using the input DAGs. + * + * @see Utils#dSeparated(Dag, Node, Node, List) + */ private Dag outputDag; + + /** + * A map to store the local scores for edge deletions. + * This map is used to cache the scores of edge deletions to avoid redundant calculations. + * The key is a string representation of the edge and its conditioning set, and the value is the score. + */ private final Map localScore = new HashMap<>(); - private int numberOfInsertedEdges = 0; + /** + * Number of edges inserted during the consensus union and backward equivalence search process. + * This variable keeps track of the total number of edges that were added to the consensus DAG + * during the union of transformed input DAGs and the subsequent edge deletions. + * + * @see ConsensusUnion#getNumberOfInsertedEdges() + * @see BackwardEquivalenceSearchDSep#applyBackwardEliminationWithDSeparation() + */ + private int numberOfInsertedEdges = 0; + /** + * Constructor for BackwardEquivalenceSearchDSep that initializes the properties for the search with a union DAG and lists of initial and transformed DAGs. + * + * @param union The resulting union DAG from the ConsensusUnion process. + * @param initialDags List of initial DAGs used to check the d-separation condition. + * @param transformedDags List of transformed DAGs after applying the alpha order. + */ public BackwardEquivalenceSearchDSep(Dag union, ArrayListinitialDags, ArrayList transformedDags) { this.graph = new EdgeListGraph(new LinkedList<>(union.getNodes())); for (Edge edge : union.getEdges()) { @@ -38,6 +100,12 @@ public BackwardEquivalenceSearchDSep(Dag union, ArrayListinitialDags, Array this.transformedDags = transformedDags; } + /** + * Applies the Backward Equivalence Search with D-separation to the consensus DAG. + * This method iteratively removes edges from the consensus DAG while ensuring that the d-separation condition is maintained across all input DAGs. + * It returns the final output DAG after all possible edge deletions. + * @return The output DAG after applying the Backward Equivalence Search with D-separation. + */ public Dag applyBackwardEliminationWithDSeparation(){ double score = 0; EdgeCandidate bestCandidate; @@ -103,35 +171,24 @@ public Dag applyBackwardEliminationWithDSeparation(){ return outputDag; } - private void createOutputDag() { - // Rebuild the pattern to ensure the final graph is a DAG - pdagToDag(graph); - - // Rebuild the output DAG from the final graph - this.outputDag = new Dag(); - for (Node node : graph.getNodes()) this.outputDag.addNode(node); - Node nodeT, nodeH; - for (Edge e : graph.getEdges()){ - if(!e.isDirected()) continue; - Endpoint endpoint1 = e.getEndpoint1(); - if (endpoint1.equals(Endpoint.ARROW)){ - nodeT = e.getNode1(); - nodeH = e.getNode2(); - }else{ - nodeT = e.getNode2(); - nodeH = e.getNode1(); - } - if(!this.outputDag.paths().existsDirectedPath(nodeT, nodeH)) this.outputDag.addEdge(e); - } - } - - + /** + * Rebuilds the input graph to ensure it is a valid pattern. + * This method applies the Meek rules to orient the edges and ensure that the graph is a valid pattern. + * It also converts the graph to a PDAG (Partially Directed Acyclic Graph) + * @param graph The graph to validate and rebuild as a PDAG. + */ private void rebuildPattern(Graph graph) { GraphSearchUtils.basicCpdag(graph); pdag(graph); } - + + /** + * Cleans the undirected edges in the graph by converting them to directed edges. + * This method iterates through the edges of the graph and transforms undirected edges into two directed edges, + * ensuring that the resulting graph maintains only directed edges. + * @return + */ private List cleanUndirectedEdges() { Set edges1 = graph.getEdges(); List edges = new ArrayList<>(); @@ -150,6 +207,14 @@ private List cleanUndirectedEdges() { return edges; } + /** + * Calculates the best candidate edge for deletion based on the current score and the edges available. + * This method evaluates each edge and its possible conditioning sets to find the edge that, when deleted, + * results in the highest score improvement while maintaining the d-separation condition. + * @param edges List of edges to consider for deletion. + * @param score The current score before any edge deletion. + * @return An EdgeCandidate object representing the best edge to delete, or null if no suitable edge is found. + */ private EdgeCandidate calculateBestCandidateEdge(List edges, double score){ double bestScore = score; EdgeCandidate bestCandidate = null; @@ -195,6 +260,14 @@ private EdgeCandidate calculateBestCandidateEdge(List edges, double score) return bestCandidate; } + /** + * Executes the deletion of the best candidate edge from the graph. + * This method removes the edge from the graph and updates the local score map. + * It also rebuilds the pattern after the deletion and updates the number of inserted edges. + * @param bestCandidate The best candidate edge to delete, containing the tail, head, conditioning set, and score. + * @return The score after the edge deletion is executed. + * This score reflects the new state of the graph after the edge has been removed. + */ private double executeEdgeDeletion(EdgeCandidate bestCandidate) { Node bestTail; Node bestHead; @@ -227,17 +300,46 @@ private double executeEdgeDeletion(EdgeCandidate bestCandidate) { return score; } + /** + * Creates the output DAG from the final graph after applying the Backward Equivalence Search. + * This method ensures that the final graph is a valid DAG by removing any cycles and undirected edges. + * It converts the graph from a PDAG to a DAG and rebuilds the output DAG from the final graph. + * The output DAG contains all nodes and directed edges, ensuring that it is acyclic. + * + * @see Utils#pdagToDag(Graph) + * @see Dag + */ + private void createOutputDag() { + // Rebuild the pattern to ensure the final graph is a DAG + pdagToDag(graph); + + // Rebuild the output DAG from the final graph + this.outputDag = new Dag(); + for (Node node : graph.getNodes()) this.outputDag.addNode(node); + Node nodeT, nodeH; + for (Edge e : graph.getEdges()){ + if(!e.isDirected()) continue; + Endpoint endpoint1 = e.getEndpoint1(); + if (endpoint1.equals(Endpoint.ARROW)){ + nodeT = e.getNode1(); + nodeH = e.getNode2(); + }else{ + nodeT = e.getNode2(); + nodeH = e.getNode1(); + } + if(!this.outputDag.paths().existsDirectedPath(nodeT, nodeH)) this.outputDag.addEdge(e); + } + } + /** - * Fully direct a graph with background knowledge. I am not sure how to - * adapt Chickering's suggested algorithm above (dagToPdag) to incorporate - * background knowledge, so I am also implementing this algorithm based on - * Meek's 1995 UAI paper. Notice it is the same implemented in PcSearch. - *

*IMPORTANT!* *It assumes all colliders are oriented, as well as + * Transforms a dag into a pdag assuming that all colliders are oriented, as well as * arrows dictated by time order.* - * - * ELIMINADO BACKGROUND KNOWLEDGE + * @param graph The graph to transform into a PDAG. + * @see MeekRules + * @see GraphSearchUtils#basicCpdag(Graph) + * */ private void pdag(Graph graph) { MeekRules rules = new MeekRules(); @@ -245,6 +347,7 @@ private void pdag(Graph graph) { rules.orientImplied(graph); } + private static List getHNeighbors(Node x, Node y, Graph graph) { List hNeighbors = new LinkedList<>(graph.getAdjacentNodes(y)); hNeighbors.retainAll(graph.getAdjacentNodes(x)); @@ -308,7 +411,7 @@ private double scoreGraphChangeDelete(Node y, Node x, Set set){ LinkedList conditioning = new LinkedList<>(); conditioning.addAll(set); for(Dag g: this.initialDags){ - if(!dSeparated(g,y, x, conditioning)) return 0.0; + if(!Utils.dSeparated(g,y, x, conditioning)) return 0.0; } eval = 1.0; //eval / (double) this.setOfdags.size(); val = eval; @@ -319,103 +422,7 @@ private double scoreGraphChangeDelete(Node y, Node x, Set set){ } } - boolean dSeparated(Dag g, Node x, Node y, LinkedList cond){ - - LinkedList open = new LinkedList(); - HashMap close = new HashMap(); - open.add(x); - open.add(y); - open.addAll(cond); - while (open.size() != 0){ - Node a = open.getFirst(); - open.remove(a); - close.put(a.toString(),a); - List pa =g.getParents(a); - for(Node p : pa){ - if(close.get(p.toString()) == null){ - if(!open.contains(p)) open.addLast(p); - } - } - } - Graph aux = new EdgeListGraph(); - - for (Node node : g.getNodes()) aux.addNode(node); - Node nodeT, nodeH; - for (Edge e : g.getEdges()){ - if(!e.isDirected()) continue; - nodeT = e.getNode1(); - nodeH = e.getNode2(); - if((close.get(nodeH.toString())!=null)&&(close.get(nodeT.toString())!=null)){ - Edge newEdge = new Edge(e.getNode1(),e.getNode2(),e.getEndpoint1(),e.getEndpoint2()); - aux.addEdge(newEdge); - } - } - - close = new HashMap(); - for(Edge e: aux.getEdges()){ - if(e.isDirected()){ - Node h; - if(e.getEndpoint1()==Endpoint.ARROW){ - h = e.getNode1(); - }else h = e.getNode2(); - if(close.get(h.toString())==null){ - close.put(h.toString(),h); - List pa = aux.getParents(h); - if(pa.size()>1){ - for(int i = 0 ; i< pa.size() - 1; i++) - for(int j = i+1; j < pa.size(); j++){ - Node p1 = pa.get(i); - Node p2 = pa.get(j); - boolean found = false; - for(Edge edge : aux.getEdges()){ - if(edge.getNode1().equals(p1)&&(edge.getNode2().equals(p2))){ - found = true; - break; - } - if(edge.getNode2().equals(p1)&&(edge.getNode1().equals(p2))){ - found = true; - break; - } - } - if(!found) aux.addUndirectedEdge(p1, p2); - } - } - - } - } - } - - for(Edge e: aux.getEdges()){ - if(e.isDirected()){ - e.setEndpoint1(Endpoint.TAIL); - e.setEndpoint2(Endpoint.TAIL); - } - } - - aux.removeNodes(cond); - - open = new LinkedList(); - close = new HashMap(); - open.add(x); - while (open.size() != 0){ - Node a = open.getFirst(); - if(a.equals(y)) return false; - open.remove(a); - close.put(a.toString(),a); - List pa =aux.getAdjacentNodes(a); - for(Node p : pa){ - if(p == null) continue; - if(close.get(p.toString()) == null){ - if(!open.contains(p)) open.addLast(p); - } - } - } - - return true; - } - - private static boolean isClique(List set, Graph graph) { List setv = new LinkedList(set); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java index d4c7a7f..0831f93 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java @@ -1,10 +1,16 @@ package es.uclm.i3a.simd.consensusBN; +import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; import edu.cmu.tetrad.graph.EdgeListGraph; +import edu.cmu.tetrad.graph.Endpoint; import edu.cmu.tetrad.graph.Graph; import edu.cmu.tetrad.graph.GraphUtils; import edu.cmu.tetrad.graph.Node; @@ -64,5 +70,108 @@ public static void pdagToDag(Graph graph){ nodes.remove(x); }while(nodes.size() > 0); } + + + public static boolean dSeparated(Dag g, Node x, Node y, List cond) { + + Set relevantNodes = findRelevantNodes(g, x, y, cond); + Graph aux = buildInducedSubgraph(g, relevantNodes); + moralize(aux); + convertToUndirected(aux); + aux.removeNodes(cond); + return !isReachable(aux, x, y); + } + + private static Set findRelevantNodes(Dag g, Node x, Node y, List cond) { + Set visited = new HashSet<>(); + Deque stack = new ArrayDeque<>(); + + stack.push(x); + stack.push(y); + for (Node c : cond) stack.push(c); + + while (!stack.isEmpty()) { + Node current = stack.pop(); + if (visited.add(current)) { + for (Node parent : g.getParents(current)) { + stack.push(parent); + } + } + } + + return visited; + } + + private static Graph buildInducedSubgraph(Dag g, Set nodesToKeep) { + Graph subgraph = new EdgeListGraph(); + + for (Node node : g.getNodes()) { + if (nodesToKeep.contains(node)) { + subgraph.addNode(node); + } + } + + for (Edge e : g.getEdges()) { + if (!e.isDirected()) continue; + + Node tail = e.getNode1(); + Node head = e.getNode2(); + + if (nodesToKeep.contains(tail) && nodesToKeep.contains(head)) { + subgraph.addEdge(new Edge(tail, head, e.getEndpoint1(), e.getEndpoint2())); + } + } + + return subgraph; + } + + private static void moralize(Graph graph) { + for (Node child : graph.getNodes()) { + List parents = graph.getParents(child); + int n = parents.size(); + if (n <= 1) continue; + + for (int i = 0; i < n - 1; i++) { + for (int j = i + 1; j < n; j++) { + Node p1 = parents.get(i); + Node p2 = parents.get(j); + if (!graph.isAdjacentTo(p1, p2)) { + graph.addUndirectedEdge(p1, p2); + } + } + } + } + } + + private static void convertToUndirected(Graph graph) { + for (Edge e : new ArrayList<>(graph.getEdges())) { + if (e.isDirected()) { + e.setEndpoint1(Endpoint.TAIL); + e.setEndpoint2(Endpoint.TAIL); + } + } + } + + private static boolean isReachable(Graph g, Node start, Node target) { + Set visited = new HashSet<>(); + Deque stack = new ArrayDeque<>(); + stack.push(start); + + while (!stack.isEmpty()) { + Node current = stack.pop(); + if (current.equals(target)) return true; + if (visited.add(current)) { + for (Node neighbor : g.getAdjacentNodes(current)) { + if (!visited.contains(neighbor)) { + stack.push(neighbor); + } + } + } + } + + return false; + } + + } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java new file mode 100644 index 0000000..55f44d6 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java @@ -0,0 +1,102 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Edge; +import edu.cmu.tetrad.graph.Edges; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + + +public class UtilsTest { + // d-separation tests + + private Node node(String name) { + return new GraphNode(name); + } + + private Dag createDag(Edge... edges) { + Dag dag = new Dag(); + for (Edge edge : edges) { + dag.addNode(edge.getNode1()); + dag.addNode(edge.getNode2()); + dag.addDirectedEdge(edge.getNode1(), edge.getNode2()); + } + return dag; + } + + @Test + public void testDirectConnection() { + Node A = node("A"), B = node("B"); + Dag dag = createDag(Edges.directedEdge(A, B)); + List conditioning = Collections.emptyList(); + assertFalse(Utils.dSeparated(dag, A, B, conditioning)); + } + + @Test + public void testChainNoCondition() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(B, C)); + List conditioning = Collections.emptyList(); + assertFalse(Utils.dSeparated(dag, A, C, conditioning)); + } + + @Test + public void testChainWithCondition() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(B, C)); + + assertTrue(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); + } + + @Test + public void testColliderNoCondition() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, B)); + List conditioning = Collections.emptyList(); + assertTrue(Utils.dSeparated(dag, A, C, conditioning)); + } + + @Test + public void testColliderConditionedOnCollider() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, B)); + + assertFalse(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); + } + + @Test + public void testDivergingNoCondition() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(B, A), Edges.directedEdge(B, C)); + List conditioning = Collections.emptyList(); + assertFalse(Utils.dSeparated(dag, A, C, conditioning)); + } + + @Test + public void testDivergingConditionedOnCommonParent() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(B, A), Edges.directedEdge(B, C)); + + assertTrue(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); + } + + @Test + public void testColliderConditionedOnDescendant() { + Node A = node("A"), B = node("B"), C = node("C"), D = node("D"); + Dag dag = createDag( + Edges.directedEdge(A, B), + Edges.directedEdge(C, B), + Edges.directedEdge(B, D) + ); + + assertFalse(Utils.dSeparated(dag, A, C, Collections.singletonList(D))); + } + +} From 6888065e6b667a97f3e66b41c010890c189b679a Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:05:46 +0200 Subject: [PATCH 10/32] Moving findNaXY to Utils as static and cleaning BESd --- .../uclm/i3a/simd/consensusBN/AlphaOrder.java | 1 - .../BackwardEquivalenceSearchDSep.java | 80 ++++++----- .../consensusBN/HeuristicConsensusBES.java | 29 +--- .../consensusBN/PairWiseConsensusBES.java | 5 +- .../es/uclm/i3a/simd/consensusBN/Utils.java | 28 ++++ .../BackwardEquivalenceSearchDSepTest.java | 80 +++++------ .../uclm/i3a/simd/consensusBN/UtilsTest.java | 124 ++++++++++++++++++ 7 files changed, 250 insertions(+), 97 deletions(-) diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java b/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java index 7865c15..35f99ac 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java @@ -253,7 +253,6 @@ private void coverEdge(Dag g, Node nodeAlpha, Node child) { Edge pay = g.getEdge(nodep, child); if(pay == null) g.addEdge(new Edge(nodep,child,Endpoint.TAIL,Endpoint.ARROW)); - } // Adding edges from parents of child to nodeAlpha. diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index 5331c50..06f12a0 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -231,7 +231,7 @@ private EdgeCandidate calculateBestCandidateEdge(List edges, double score) SubSet hSubset=hSubsets.nextElement(); // Checking if {naYXH} \ {hSubset} is a clique - List naYXH = findNaYX(candidateTail, candidateHead, graph); + List naYXH = Utils.findNaYX(candidateTail, candidateHead, graph); naYXH.removeAll(hSubset); if (!isClique(naYXH, graph)) { continue; @@ -348,6 +348,16 @@ private void pdag(Graph graph) { } + /** + * Finds all neighbors of node x that are adjacent to node y in the graph. + * This method retrieves the neighbors of node y that are also adjacent to node x, + * ensuring that the edges between them are undirected. + * It filters out undirected edges to ensure that only neighbors from directed edges are considered. + * @param x Node x to find neighbors for. + * @param y Node y to find neighbors for. + * @param graph The graph in which to find the neighbors. + * @return A list of nodes that are neighbors of x and y, filtered to include only neighbors from directed edges. + */ private static List getHNeighbors(Node x, Node y, Graph graph) { List hNeighbors = new LinkedList<>(graph.getAdjacentNodes(y)); hNeighbors.retainAll(graph.getAdjacentNodes(x)); @@ -363,45 +373,53 @@ private static List getHNeighbors(Node x, Node y, Graph graph) { return hNeighbors; } - private static void delete(Node x, Node y, Set subset, Graph graph) { - graph.removeEdges(x, y); + /** + * Applies the delete operation from Chickering 2002 for the edge x->y in the graph, and updates the edges + * connecting x and y to the nodes in the provided subset. This is done to ensure that the same dependency structure is maintained + * while removing the edge between x and y. + * @param tailNode The tail node of the edge to be deleted. + * @param headNode The head node of the edge to be deleted. + * @param subset The set of nodes that will be connected to the tail and head nodes after the deletion. + * @param graph The graph from which the edge is deleted and the connections are updated. + */ + private static void delete(Node tailNode, Node headNode, Set subset, Graph graph) { + graph.removeEdges(tailNode, headNode); for (Node aSubset : subset) { - if (!graph.isParentOf(aSubset, x) && !graph.isParentOf(x, aSubset)) { - graph.removeEdge(x, aSubset); - graph.addDirectedEdge(x, aSubset); + if (!graph.isParentOf(aSubset, tailNode) && !graph.isParentOf(tailNode, aSubset)) { + graph.removeEdge(tailNode, aSubset); + graph.addDirectedEdge(tailNode, aSubset); } - graph.removeEdge(y, aSubset); - graph.addDirectedEdge(y, aSubset); + graph.removeEdge(headNode, aSubset); + graph.addDirectedEdge(headNode, aSubset); } } - private double deleteEval(Node x, Node y, SubSet h, Graph graph){ - - Set set1 = new HashSet(findNaYX(x, y, graph)); - set1.removeAll(h); - set1.addAll(graph.getParents(y)); - set1.remove(x); - return scoreGraphChangeDelete(y, x, set1); // calcular si y esta d-separado de x dado el set1 en cada grafo. + /** + * Evaluates the impact of deleting an edge from the graph based on d-separation. + * + * This method computes a score for deleting the edge from {@code x} to {@code y}, + * taking into account a conditioning set of nodes {@code conditioningSet}. It uses + * structural information from the graph to assess whether {@code y} is d-separated + * from {@code x} given the constructed conditioning set. + * + * @param x The source node of the edge to be deleted. + * @param y The target node of the edge to be deleted. + * @param conditioningSet The set of nodes used as conditioning variables (Z) for d-separation. + * @param graph The graph in which the change is being evaluated. + * @return The score resulting from deleting the edge, based on the given context. + */ + private double deleteEval(Node x, Node y, SubSet conditioningSet, Graph graph){ + // Setup the conditioning set for d-separation by removing the conditioning nodes from the naYX set, adding the parents of y and removing x. + Set finalConditioningSet = new HashSet<>(Utils.findNaYX(x, y, graph)); + finalConditioningSet.removeAll(conditioningSet); + finalConditioningSet.addAll(graph.getParents(y)); + finalConditioningSet.remove(x); + // Check if y is d-separated from x given the final conditioning set in each graph. + return scoreGraphChangeDelete(y, x, finalConditioningSet); } - private static List findNaYX(Node x, Node y, Graph graph) { - List naYX = new LinkedList<>(graph.getAdjacentNodes(y)); - naYX.retainAll(graph.getAdjacentNodes(x)); - - for (int i = naYX.size()-1; i >= 0; i--) { - Node z = naYX.get(i); - Edge edge = graph.getEdge(y, z); - - if (!Edges.isUndirectedEdge(edge)) { - naYX.remove(z); - } - } - - return naYX; - } - private double scoreGraphChangeDelete(Node y, Node x, Set set){ String key = y.getName()+x.getName()+set.toString(); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java index adddc2a..8d2a496 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java @@ -40,12 +40,12 @@ public class HeuristicConsensusBES { public HeuristicConsensusBES(ArrayList dags, double percentage){ this.setOfdags = dags; this.heuristic = new AlphaOrder(this.setOfdags); - this.heuristic.computeAlphaH2(); - this.alpha = this.heuristic.alpha; + this.heuristic.computeAlpha(); + this.alpha = this.heuristic.getOrder(); this.imaps2alpha = new TransformDags(this.setOfdags,this.alpha); this.imaps2alpha.transform(); this.numberOfInsertedEdges = imaps2alpha.getNumberOfInsertedEdges(); - this.setOfOutDags = imaps2alpha.setOfOutputDags; + this.setOfOutDags = imaps2alpha.getSetOfOutputDags(); this.percentage = percentage; } @@ -58,7 +58,7 @@ private void consensusUnion(){ this.union = new Dag(this.alpha); for(Node nodei: this.alpha){ - for(Dag d : this.imaps2alpha.setOfOutputDags){ + for(Dag d : this.imaps2alpha.getSetOfOutputDags()){ Listparent = d.getParents(nodei); for(Node pa: parent){ if(!this.union.isParentOf(pa, nodei)) this.union.addEdge(new Edge(pa,nodei,Endpoint.TAIL,Endpoint.ARROW)); @@ -128,7 +128,7 @@ public void fusion(){ } // INICIO TEST 1 - List naYXH = findNaYX(_x, _y, graph); + List naYXH = Utils.findNaYX(_x, _y, graph); naYXH.removeAll(hSubset); if (!isClique(naYXH, graph)) { // hSubsets.firstTest(true); // Si pasa para H entonces pasa para cualquier H' | H' contiene H @@ -250,7 +250,7 @@ private static List getHNeighbors(Node x, Node y, Graph graph) { double deleteEval(Node x, Node y, SubSet h, Graph graph){ - Set set1 = new HashSet(findNaYX(x, y, graph)); + Set set1 = new HashSet(Utils.findNaYX(x, y, graph)); set1.removeAll(h); set1.addAll(graph.getParents(y)); set1.remove(x); @@ -374,23 +374,6 @@ boolean dSeparated(Dag g, Node x, Node y, LinkedList cond){ return true; } - - - private static List findNaYX(Node x, Node y, Graph graph) { - List naYX = new LinkedList(graph.getAdjacentNodes(y)); - naYX.retainAll(graph.getAdjacentNodes(x)); - - for (int i = naYX.size()-1; i >= 0; i--) { - Node z = naYX.get(i); - Edge edge = graph.getEdge(y, z); - - if (!Edges.isUndirectedEdge(edge)) { - naYX.remove(z); - } - } - - return naYX; - } public Dag getFusion(){ diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBES.java index 3b053bb..bbd639e 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBES.java @@ -2,7 +2,6 @@ import java.util.ArrayList; - import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; import edu.cmu.tetrad.graph.Node; @@ -31,7 +30,7 @@ public void getFusion(){ conBES = new ConsensusBES(setOfDags); conBES.fusion(); this.numberOfInsertedEdges = conBES.getNumberOfInsertedEdges(); - this.numberOfUnionEdges = conBES.union.getNumEdges(); + this.numberOfUnionEdges = conBES.getUnion().getNumEdges(); this.conDAG = conBES.getFusion(); } @@ -49,7 +48,7 @@ public int getHammingDistance(){ for(Edge ed: this.conDAG.getEdges()){ Node tail = ed.getNode1(); Node head = ed.getNode2(); - for(Dag g: conBES.setOfOutDags){ + for(Dag g: conBES.getTransformedDags()){ Edge edge1 = g.getEdge(tail, head); Edge edge2 = g.getEdge(head, tail); if(edge1 == null && edge2==null) distance++; diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java index 0831f93..3b3fa02 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java @@ -4,12 +4,14 @@ import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Set; import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; import edu.cmu.tetrad.graph.EdgeListGraph; +import edu.cmu.tetrad.graph.Edges; import edu.cmu.tetrad.graph.Endpoint; import edu.cmu.tetrad.graph.Graph; import edu.cmu.tetrad.graph.GraphUtils; @@ -173,5 +175,31 @@ private static boolean isReachable(Graph g, Node start, Node target) { } + /** + * Finds the nodes that are neighbors of node y and x in the graph. + * This method retrieves the neighbors of node y that are also adjacent to node x, + * ensuring that the edges between them are directed. + * It filters out undirected edges to ensure that only neighbors from directed edges are considered. + * @param x Node x to find neighbors for. + * @param y Node y to find neighbors for. + * @param graph The graph in which to find the neighbors. + * @return A list of nodes that are neighbors of x and y, filtered to include only neighbors from directed edges. + */ + public static List findNaYX(Node x, Node y, Graph graph) { + List naYX = new LinkedList<>(graph.getAdjacentNodes(y)); + naYX.retainAll(graph.getAdjacentNodes(x)); + + for (int i = naYX.size()-1; i >= 0; i--) { + Node z = naYX.get(i); + Edge edge = graph.getEdge(y, z); + + if (!Edges.isUndirectedEdge(edge)) { + naYX.remove(z); + } + } + + return naYX; + } + } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java index 7078928..1e8c626 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java @@ -16,39 +16,28 @@ class BackwardEquivalenceSearchDSepTest { - private Dag createSimpleDag() { - // A -> B -> C - Node a = new GraphNode("A"); - Node b = new GraphNode("B"); - Node c = new GraphNode("C"); - - Dag dag = new Dag(); - dag.addNode(a); - dag.addNode(b); - dag.addNode(c); - dag.addDirectedEdge(a, b); - dag.addDirectedEdge(b, c); + private ArrayList createRandomDagList(int copies) { + RandomBN setOfDags = new RandomBN(0, 20, 50, + copies,3); + setOfDags.setMaxInDegree(4); + setOfDags.setMaxOutDegree(4); + setOfDags.generate(); - return dag; - } - - private ArrayList createDagList(int copies) { - ArrayList list = new ArrayList<>(); - for (int i = 0; i < copies; i++) { - list.add(createSimpleDag()); - } - return list; + return setOfDags.setOfRandomDags; } @Test void testApplyBESdDoesNotThrow() { - Dag unionDag = createSimpleDag(); - ArrayList initialDags = createDagList(3); - ArrayList transformedDags = createDagList(3); - - BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, transformedDags); - + // Setting up consensus union + ArrayList initialDags = createRandomDagList(3); + ConsensusUnion consensusUnion = new ConsensusUnion(initialDags); + Dag unionDag = consensusUnion.union(); + + // Running Backward Equivalence Search with d-separation + BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, consensusUnion.getTransformedDags()); + + // No exceptions should be thrown during the process assertDoesNotThrow(() -> { Dag output = besd.applyBackwardEliminationWithDSeparation(); assertNotNull(output); @@ -57,11 +46,13 @@ void testApplyBESdDoesNotThrow() { @Test void testOutputIsDAG() { - Dag unionDag = createSimpleDag(); - ArrayList initialDags = createDagList(2); - ArrayList transformedDags = createDagList(2); - - BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, transformedDags); + // Setting up consensus union + ArrayList initialDags = createRandomDagList(3); + ConsensusUnion consensusUnion = new ConsensusUnion(initialDags); + Dag unionDag = consensusUnion.union(); + + // Running Backward Equivalence Search with d-separation + BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, consensusUnion.getTransformedDags()); Dag outputDag = besd.applyBackwardEliminationWithDSeparation(); assertTrue(GraphUtils.isDag(outputDag), "El resultado no es un DAG válido."); @@ -82,12 +73,19 @@ void testAristasSePuedenEliminar() { Dag dag1 = new Dag(); dag1.addNode(a); dag1.addNode(b); + + Dag dag2 = new Dag(); + dag2.addNode(a); + dag2.addNode(b); + // Aquí no hay aristas, A y B están desconectados // sin conexión ArrayList initialDags = new ArrayList<>(); initialDags.add(dag1); - ArrayList transformedDags = new ArrayList<>(); - transformedDags.add(dag1); + initialDags.add(dag2); + AlphaOrder alphaOrder = new AlphaOrder(initialDags); + alphaOrder.computeAlpha(); + ArrayList transformedDags = (new TransformDags(initialDags, alphaOrder.getOrder())).transform(); BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, transformedDags); Dag outputDag = besd.applyBackwardEliminationWithDSeparation(); @@ -99,14 +97,18 @@ void testAristasSePuedenEliminar() { @Test void testGetNumberOfInsertedEdgesReflectsChanges() { - Dag unionDag = createSimpleDag(); - ArrayList dags = createDagList(2); + ArrayList initialDags = createRandomDagList(2); + ConsensusUnion consensusUnion = new ConsensusUnion(initialDags); + Dag unionDag = consensusUnion.union(); + ArrayList transformedDags = consensusUnion.getTransformedDags(); + int insertedEdgesBefore = consensusUnion.getNumberOfInsertedEdges(); - BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, dags, dags); + BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, transformedDags); besd.applyBackwardEliminationWithDSeparation(); - int insertedEdges = besd.getNumberOfInsertedEdges(); + int insertedEdgesAfter = besd.getNumberOfInsertedEdges(); // En el peor de los casos no ha eliminado ninguna, pero nunca debe ser negativo - assertTrue(insertedEdges >= 0, "El número de aristas insertadas no puede ser negativo."); + assertTrue(insertedEdgesAfter >= 0, "The number of inserted edges should not be negative."); + assertTrue(insertedEdgesAfter <= insertedEdgesBefore, "The number of inserted edges should decrease after BES."); } } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java index 55f44d6..89b43ad 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java @@ -3,13 +3,17 @@ import java.util.Collections; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; +import edu.cmu.tetrad.graph.EdgeListGraph; import edu.cmu.tetrad.graph.Edges; +import edu.cmu.tetrad.graph.Endpoint; +import edu.cmu.tetrad.graph.Graph; import edu.cmu.tetrad.graph.GraphNode; import edu.cmu.tetrad.graph.Node; @@ -99,4 +103,124 @@ public void testColliderConditionedOnDescendant() { assertFalse(Utils.dSeparated(dag, A, C, Collections.singletonList(D))); } + //find naYX tests + + @Test + public void testFindNaYX_singleUndirectedCommonNeighbor() { + Graph graph = new EdgeListGraph(); + + Node x = new GraphNode("X"); + Node y = new GraphNode("Y"); + Node z = new GraphNode("Z"); + + graph.addNode(x); + graph.addNode(y); + graph.addNode(z); + + graph.addEdge(new Edge(x, z, Endpoint.TAIL, Endpoint.TAIL)); // undirected + graph.addEdge(new Edge(y, z, Endpoint.TAIL, Endpoint.TAIL)); // undirected + + List result = Utils.findNaYX(x, y, graph); + assertEquals(1, result.size()); + assertTrue(result.contains(z)); + } + + @Test + public void testFindNaYX_directedEdgeShouldBeExcluded() { + Graph graph = new EdgeListGraph(); + + Node x = new GraphNode("X"); + Node y = new GraphNode("Y"); + Node z = new GraphNode("Z"); + + graph.addNode(x); + graph.addNode(y); + graph.addNode(z); + + graph.addEdge(new Edge(x, z, Endpoint.ARROW, Endpoint.TAIL)); // x → z + graph.addEdge(new Edge(y, z, Endpoint.ARROW, Endpoint.TAIL)); // y → z + + List result = Utils.findNaYX(x, y, graph); + assertEquals(0, result.size()); + } + + @Test + public void testFindNaYX_mixedNeighbors() { + Graph graph = new EdgeListGraph(); + + Node x = new GraphNode("X"); + Node y = new GraphNode("Y"); + Node z1 = new GraphNode("Z1"); // undirected common + Node z2 = new GraphNode("Z2"); // directed common + Node z3 = new GraphNode("Z3"); // only adjacent to x + + graph.addNode(x); + graph.addNode(y); + graph.addNode(z1); + graph.addNode(z2); + graph.addNode(z3); + + // z1: undirected edge with both + graph.addEdge(new Edge(x, z1, Endpoint.TAIL, Endpoint.TAIL)); + graph.addEdge(new Edge(y, z1, Endpoint.TAIL, Endpoint.TAIL)); + + // z2: directed edges with both + graph.addEdge(new Edge(x, z2, Endpoint.TAIL, Endpoint.ARROW)); + graph.addEdge(new Edge(y, z2, Endpoint.TAIL, Endpoint.ARROW)); + + // z3: only adjacent to x + graph.addEdge(new Edge(x, z3, Endpoint.TAIL, Endpoint.TAIL)); + + List result = Utils.findNaYX(x, y, graph); + assertEquals(1, result.size()); + assertTrue(result.contains(z1)); + assertFalse(result.contains(z2)); + assertFalse(result.contains(z3)); + } + + @Test + public void testFindNaYX_multipleUndirectedCommonNeighbors() { + Graph graph = new EdgeListGraph(); + + Node x = new GraphNode("X"); + Node y = new GraphNode("Y"); + Node a = new GraphNode("A"); + Node b = new GraphNode("B"); + + graph.addNode(x); + graph.addNode(y); + graph.addNode(a); + graph.addNode(b); + + graph.addEdge(new Edge(x, a, Endpoint.TAIL, Endpoint.TAIL)); + graph.addEdge(new Edge(y, a, Endpoint.TAIL, Endpoint.TAIL)); + graph.addEdge(new Edge(x, b, Endpoint.TAIL, Endpoint.TAIL)); + graph.addEdge(new Edge(y, b, Endpoint.TAIL, Endpoint.TAIL)); + + List result = Utils.findNaYX(x, y, graph); + assertEquals(2, result.size()); + assertTrue(result.contains(a)); + assertTrue(result.contains(b)); + } + + @Test + public void testFindNaYX_noCommonNeighbors() { + Graph graph = new EdgeListGraph(); + + Node x = new GraphNode("X"); + Node y = new GraphNode("Y"); + Node a = new GraphNode("A"); + Node b = new GraphNode("B"); + + graph.addNode(x); + graph.addNode(y); + graph.addNode(a); + graph.addNode(b); + + graph.addEdge(new Edge(x, a, Endpoint.TAIL, Endpoint.TAIL)); + graph.addEdge(new Edge(y, b, Endpoint.TAIL, Endpoint.TAIL)); + + List result = Utils.findNaYX(x, y, graph); + assertTrue(result.isEmpty()); + } } From f5873773f7501ea178b42b39f3a2d39522ffd2ef Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:06:48 +0200 Subject: [PATCH 11/32] Adding DSeparationKey for local map optimization --- .../BackwardEquivalenceSearchDSep.java | 34 +++---- .../i3a/simd/consensusBN/DSeparationKey.java | 60 ++++++++++++ .../simd/consensusBN/DSeparationKeyTest.java | 91 +++++++++++++++++++ .../uclm/i3a/simd/consensusBN/UtilsTest.java | 17 ++++ 4 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 src/main/java/es/uclm/i3a/simd/consensusBN/DSeparationKey.java create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/DSeparationKeyTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index 06f12a0..a7f08c5 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -72,7 +72,7 @@ public class BackwardEquivalenceSearchDSep { * This map is used to cache the scores of edge deletions to avoid redundant calculations. * The key is a string representation of the edge and its conditioning set, and the value is the score. */ - private final Map localScore = new HashMap<>(); + private final Map localScore = new HashMap<>(); /** * Number of edges inserted during the consensus union and backward equivalence search process. @@ -420,24 +420,24 @@ private double deleteEval(Node x, Node y, SubSet conditioningSet, Graph graph){ return scoreGraphChangeDelete(y, x, finalConditioningSet); } - private double scoreGraphChangeDelete(Node y, Node x, Set set){ - - String key = y.getName()+x.getName()+set.toString(); - Double val = this.localScore.get(key); - if(val == null){ - double eval = 0.0; - LinkedList conditioning = new LinkedList<>(); - conditioning.addAll(set); - for(Dag g: this.initialDags){ - if(!Utils.dSeparated(g,y, x, conditioning)) return 0.0; + private double scoreGraphChangeDelete(Node y, Node x, Set conditioningSet) { + DSeparationKey key = new DSeparationKey(y, x, conditioningSet); + Double cached = localScore.get(key); + + if (cached != null) { + return cached; + } + + // Evaluamos d-separación en todos los DAGs + for (Dag g : this.initialDags) { + if (!Utils.dSeparated(g, y, x, new ArrayList<>(conditioningSet))) { + localScore.put(key, 0.0); + return 0.0; } - eval = 1.0; //eval / (double) this.setOfdags.size(); - val = eval; - this.localScore.put(key, val); - return eval; - }else{ - return val.doubleValue(); } + + localScore.put(key, 1.0); + return 1.0; } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/DSeparationKey.java b/src/main/java/es/uclm/i3a/simd/consensusBN/DSeparationKey.java new file mode 100644 index 0000000..4e8cbb0 --- /dev/null +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/DSeparationKey.java @@ -0,0 +1,60 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import edu.cmu.tetrad.graph.Node; + +public class DSeparationKey { + private final Node y; + private final Node x; + private final Set conditioningSet; + + public DSeparationKey(Node x, Node y, Set conditioningSet) { + // Since D-separation is symmetric, we ensure a consistent order for x and y + if (x.getName().compareTo(y.getName()) <= 0) { + this.x = x; + this.y = y; + } else { + this.x = y; + this.y = x; + } + this.conditioningSet = new HashSet<>(conditioningSet); // copia defensiva + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof DSeparationKey)) return false; + + DSeparationKey other = (DSeparationKey) obj; + return y.equals(other.y) + && x.equals(other.x) + && conditioningSet.equals(other.conditioningSet); + } + + @Override + public int hashCode() { + return Objects.hash(y, x, conditioningSet); + } + + + public Node getY() { + return this.y; + } + + + public Node getX() { + return this.x; + } + + + public Set getConditioningSet() { + return Collections.unmodifiableSet(this.conditioningSet); + } + + +} + diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/DSeparationKeyTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/DSeparationKeyTest.java new file mode 100644 index 0000000..4d5d880 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/DSeparationKeyTest.java @@ -0,0 +1,91 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +class DSeparationKeyTest { + + private final Node X = new GraphNode("X"); + private final Node Y = new GraphNode("Y"); + private final Node Z = new GraphNode("Z"); + private final Node W = new GraphNode("W"); + + @Test + void testConstructorAndGetters() { + Set zSet = new HashSet<>(Arrays.asList(Z, W)); + DSeparationKey key = new DSeparationKey(X, Y, zSet); + + assertEquals(X, key.getX()); + assertEquals(Y, key.getY()); + assertEquals(new HashSet<>(Arrays.asList(Z, W)), key.getConditioningSet()); + } + + @Test + void testEqualsSameContentDifferentOrder() { + Set zSet1 = new HashSet<>(Arrays.asList(Z, W)); + Set zSet2 = new HashSet<>(Arrays.asList(W, Z)); + + DSeparationKey key1 = new DSeparationKey(X, Y, zSet1); + DSeparationKey key2 = new DSeparationKey(X, Y, zSet2); + + assertEquals(key1, key2); + assertEquals(key1.hashCode(), key2.hashCode()); + } + + @Test + void testNotEqualsDifferentZ() { + Set zSet1 = new HashSet<>(Collections.singletonList(Z)); + Set zSet2 = new HashSet<>(Arrays.asList(Z, W)); + + DSeparationKey key1 = new DSeparationKey(X, Y, zSet1); + DSeparationKey key2 = new DSeparationKey(X, Y, zSet2); + + assertNotEquals(key1, key2); + } + + @Test + void testSimmetryBetweenXandY() { + DSeparationKey key1 = new DSeparationKey(X, Y, Collections.emptySet()); + DSeparationKey key2 = new DSeparationKey(Y, X, Collections.emptySet()); + + assertEquals(key1, key2); + } + + @Test + void testEqualsSelf() { + DSeparationKey key = new DSeparationKey(X, Y, Collections.singleton(Z)); + assertEquals(key, key); + } + + @Test + void testNotEqualsNullOrDifferentClass() { + DSeparationKey key = new DSeparationKey(X, Y, Collections.singleton(Z)); + + assertNotEquals(null, key); + assertNotEquals("NotAKey", key); + } + + @Test + void testKeyAsMapKey() { + DSeparationKey key1 = new DSeparationKey(X, Y, new HashSet<>(Arrays.asList(Z, W))); + DSeparationKey key2 = new DSeparationKey(X, Y, new HashSet<>(Arrays.asList(W, Z))); + + Map map = new HashMap<>(); + map.put(key1, true); + + assertTrue(map.containsKey(key2)); + assertTrue(map.get(key2)); + } +} diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java index 89b43ad..50657cf 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java @@ -103,6 +103,23 @@ public void testColliderConditionedOnDescendant() { assertFalse(Utils.dSeparated(dag, A, C, Collections.singletonList(D))); } + // Asegurar que esto funciona correctamente!!!!!!! + @Test + public void testSimmetryBetweenXandY() { + Node A = node("A"), B = node("B"), C = node("C"), D = node("D"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, A), Edges.directedEdge(D, A)); + List conditioning = Collections.emptyList(); + + // No colliders between A and B, so they are not d-separated + assertFalse(Utils.dSeparated(dag, A, B, conditioning)); + assertFalse(Utils.dSeparated(dag, B, A, conditioning)); + + // C->A and D->A makes A a collider for C and D, and therefore C and D are d-separated from each other + assertTrue(Utils.dSeparated(dag, C, D, conditioning)); + assertTrue(Utils.dSeparated(dag, D, C, conditioning)); + + } + //find naYX tests @Test From ae6def1f14fdda2fe3af843beddd91df5e44f935 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:45:11 +0200 Subject: [PATCH 12/32] Cleaned and tested BackwardEquivalenceSearchDSep --- .../BackwardEquivalenceSearchDSep.java | 87 ++++++++++++------- .../i3a/simd/consensusBN/ConsensusBES.java | 2 +- .../BackwardEquivalenceSearchDSepTest.java | 2 +- 3 files changed, 60 insertions(+), 31 deletions(-) diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index a7f08c5..4b3334f 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -1,8 +1,6 @@ package es.uclm.i3a.simd.consensusBN; -import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; @@ -16,6 +14,7 @@ import edu.cmu.tetrad.graph.Edges; import edu.cmu.tetrad.graph.Endpoint; import edu.cmu.tetrad.graph.Graph; +import edu.cmu.tetrad.graph.GraphUtils; import edu.cmu.tetrad.graph.Node; import edu.cmu.tetrad.search.utils.GraphSearchUtils; import edu.cmu.tetrad.search.utils.MeekRules; @@ -75,14 +74,14 @@ public class BackwardEquivalenceSearchDSep { private final Map localScore = new HashMap<>(); /** - * Number of edges inserted during the consensus union and backward equivalence search process. - * This variable keeps track of the total number of edges that were added to the consensus DAG - * during the union of transformed input DAGs and the subsequent edge deletions. + * Number of edges removed during the backward equivalence search process. + * This variable keeps track of the total number of edges that are inserted (deleted) during the + * Backward Equivalence Search with D-separation process. * * @see ConsensusUnion#getNumberOfInsertedEdges() * @see BackwardEquivalenceSearchDSep#applyBackwardEliminationWithDSeparation() */ - private int numberOfInsertedEdges = 0; + private int numberOfRemovedEdges = 0; /** * Constructor for BackwardEquivalenceSearchDSep that initializes the properties for the search with a union DAG and lists of initial and transformed DAGs. @@ -233,7 +232,7 @@ private EdgeCandidate calculateBestCandidateEdge(List edges, double score) // Checking if {naYXH} \ {hSubset} is a clique List naYXH = Utils.findNaYX(candidateTail, candidateHead, graph); naYXH.removeAll(hSubset); - if (!isClique(naYXH, graph)) { + if (!GraphUtils.isClique(naYXH, graph)) { continue; } @@ -293,7 +292,7 @@ private double executeEdgeDeletion(EdgeCandidate bestCandidate) { for(int g = 0; g conditioningSet) { + /** + * Checks if the deletion of an edge from {@code x} to {@code y} maintains the d-separation condition + * across all initial DAGs. If the edge deletion maintains d-separation, it returns a score of 1.0, + * otherwise it returns 0.0. + * + * This method uses a local score map to cache results for efficiency, avoiding redundant calculations + * for the same edge and conditioning set. + * @param x The tail node of the edge to be deleted. + * @param y The head node of the edge to be deleted. + * @param conditioningSet The set of nodes used as conditioning variables (Z) for d-separation. + * @return A score of 1.0 if the edge deletion maintains d-separation, otherwise 0.0. + * + * @see Utils#dSeparated(Dag, Node, Node, List) + * @see DSeparationKey + * + * This method is crucial for ensuring that the edge deletion does not violate the d-separation condition, + * which is essential for maintaining the integrity of the Bayesian network structure. + */ + private double scoreGraphChangeDelete(Node x, Node y, Set conditioningSet) { + // Check if the edge deletion has already been evaluated and cached DSeparationKey key = new DSeparationKey(y, x, conditioningSet); Double cached = localScore.get(key); - if (cached != null) { return cached; } - // Evaluamos d-separación en todos los DAGs + // Evaluating the d-separation condition across all initial DAGs for (Dag g : this.initialDags) { - if (!Utils.dSeparated(g, y, x, new ArrayList<>(conditioningSet))) { + if (!Utils.dSeparated(g, x, y, new ArrayList<>(conditioningSet))) { localScore.put(key, 0.0); return 0.0; } @@ -439,29 +456,41 @@ private double scoreGraphChangeDelete(Node y, Node x, Set conditioningSet) localScore.put(key, 1.0); return 1.0; } - - - - private static boolean isClique(List set, Graph graph) { - List setv = new LinkedList(set); - for (int i = 0; i < setv.size() - 1; i++) { - for (int j = i + 1; j < setv.size(); j++) { - if (!graph.isAdjacentTo(setv.get(i), setv.get(j))) { - return false; - } - } - } - return true; - } - - public int getNumberOfInsertedEdges() { - return this.numberOfInsertedEdges; + /** + * Returns the number of edges that were inserted during the consensus union and backward equivalence search process. + * @return + */ + public int getNumberOfRemovedEdges() { + return this.numberOfRemovedEdges; } + /** + * Class representing a candidate edge for deletion in the Backward Equivalence Search. + * This class encapsulates the tail and head nodes of the edge, the conditioning set used for d-separation, + * and the score associated with the edge deletion. + * + * @see BackwardEquivalenceSearchDSep#applyBackwardEliminationWithDSeparation() + * @see Utils#dSeparated(Dag, Node, Node, List) + */ private class EdgeCandidate { + /** + * The tail node of the edge candidate. + */ public final Node tail; + + /** + * The head node of the edge candidate. + */ public final Node head; + + /** + * The conditioning set used for d-separation in the edge candidate. + */ public final Set conditioningSet; + + /** + * The score associated with the edge candidate deletion. + */ public double score; public EdgeCandidate(Node tail, Node head, Set conditioningSet) { diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java index d8514ef..c893d85 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java @@ -97,7 +97,7 @@ public void fusion(){ BackwardEquivalenceSearchDSep bes = new BackwardEquivalenceSearchDSep(this.union, this.inputDags, this.transformedDags); this.outputDag = bes.applyBackwardEliminationWithDSeparation(); // 3. Updating numberOfInsertedEdges - this.numberOfInsertedEdges += bes.getNumberOfInsertedEdges(); + this.numberOfInsertedEdges -= bes.getNumberOfRemovedEdges(); } /** diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java index 1e8c626..f5d6a6a 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java @@ -106,7 +106,7 @@ void testGetNumberOfInsertedEdgesReflectsChanges() { BackwardEquivalenceSearchDSep besd = new BackwardEquivalenceSearchDSep(unionDag, initialDags, transformedDags); besd.applyBackwardEliminationWithDSeparation(); - int insertedEdgesAfter = besd.getNumberOfInsertedEdges(); + int insertedEdgesAfter = insertedEdgesBefore - besd.getNumberOfRemovedEdges(); // En el peor de los casos no ha eliminado ninguna, pero nunca debe ser negativo assertTrue(insertedEdgesAfter >= 0, "The number of inserted edges should not be negative."); assertTrue(insertedEdgesAfter <= insertedEdgesBefore, "The number of inserted edges should decrease after BES."); From 6d08d809c0437d92318860f7bfbece04d5d6fc81 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:47:09 +0200 Subject: [PATCH 13/32] Splitting UtilsTest into two --- .../es/uclm/i3a/simd/consensusBN/Utils.java | 4 + .../i3a/simd/consensusBN/DseparationTest.java | 333 ++++++++++++++++++ .../{UtilsTest.java => FindNaYXTest.java} | 109 +----- 3 files changed, 340 insertions(+), 106 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/DseparationTest.java rename src/test/java/es/uclm/i3a/simd/consensusBN/{UtilsTest.java => FindNaYXTest.java} (53%) diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java index 3b3fa02..751303e 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java @@ -74,6 +74,10 @@ public static void pdagToDag(Graph graph){ } + public static boolean dSeparated(Dag g, Node x, Node y) { + return dSeparated(g, x, y, new ArrayList<>()); + } + public static boolean dSeparated(Dag g, Node x, Node y, List cond) { Set relevantNodes = findRelevantNodes(g, x, y, cond); diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/DseparationTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/DseparationTest.java new file mode 100644 index 0000000..2f92250 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/DseparationTest.java @@ -0,0 +1,333 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Edge; +import edu.cmu.tetrad.graph.Edges; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +public class DseparationTest { + // d-separation tests + + private Node node(String name) { + return new GraphNode(name); + } + + private Dag createDag(Edge... edges) { + Dag dag = new Dag(); + for (Edge edge : edges) { + dag.addNode(edge.getNode1()); + dag.addNode(edge.getNode2()); + dag.addDirectedEdge(edge.getNode1(), edge.getNode2()); + } + return dag; + } + + @Test + public void testDirectConnection() { + Node A = node("A"), B = node("B"); + Dag dag = createDag(Edges.directedEdge(A, B)); + List conditioning = Collections.emptyList(); + assertFalse(Utils.dSeparated(dag, A, B, conditioning)); + } + + @Test + public void testChainNoCondition() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(B, C)); + List conditioning = Collections.emptyList(); + assertFalse(Utils.dSeparated(dag, A, C, conditioning)); + } + + @Test + public void testChainWithCondition() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(B, C)); + + assertTrue(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); + } + + @Test + public void testColliderNoCondition() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, B)); + List conditioning = Collections.emptyList(); + assertTrue(Utils.dSeparated(dag, A, C, conditioning)); + } + + @Test + public void testColliderConditionedOnCollider() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, B)); + + assertFalse(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); + } + + @Test + public void testDivergingNoCondition() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(B, A), Edges.directedEdge(B, C)); + List conditioning = Collections.emptyList(); + assertFalse(Utils.dSeparated(dag, A, C, conditioning)); + } + + @Test + public void testDivergingConditionedOnCommonParent() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(B, A), Edges.directedEdge(B, C)); + + assertTrue(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); + } + + @Test + public void testColliderConditionedOnDescendant() { + Node A = node("A"), B = node("B"), C = node("C"), D = node("D"); + Dag dag = createDag( + Edges.directedEdge(A, B), + Edges.directedEdge(C, B), + Edges.directedEdge(B, D) + ); + + assertFalse(Utils.dSeparated(dag, A, C, Collections.singletonList(D))); + } + + @Test + public void testSimmetryBetweenXandY() { + Node A = node("A"), B = node("B"), C = node("C"), D = node("D"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, A), Edges.directedEdge(D, A)); + List conditioning = Collections.emptyList(); + + // No colliders between A and B, so they are not d-separated + assertFalse(Utils.dSeparated(dag, A, B, conditioning)); + assertFalse(Utils.dSeparated(dag, B, A, conditioning)); + + // C->A and D->A makes A a collider for C and D, and therefore C and D are d-separated from each other + assertTrue(Utils.dSeparated(dag, C, D, conditioning)); + assertTrue(Utils.dSeparated(dag, D, C, conditioning)); + + } + + @Test + public void testDseparationMethodsAreEquivalent() { + Node A = node("A"), B = node("B"), C = node("C"); + Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(B, C)); + + // Using the dSeparated method with no conditioning + assertFalse(Utils.dSeparated(dag, A, C)); + + // Using the dSeparated method with an empty conditioning set + assertFalse(Utils.dSeparated(dag, A, C, Collections.emptyList())); + } + + @Test + public void testDseparationRule1(){ + // Scenarios taken from https://yuyangyy.medium.com/understand-d-separation-471f9aada503 + // Scenario 1: Z is empty, namely, we don't dondition on any variables + // Rule 1: If there exists a path from x to y and there is no collider on the path, then x and y are not d-separated. + Node x = new GraphNode("x"); + Node r = new GraphNode("r"); + Node s = new GraphNode("s"); + Node t = new GraphNode("t"); + Node u = new GraphNode("u"); + Node v = new GraphNode("v"); + Node y = new GraphNode("y"); + + Dag dag = new Dag(); + dag.addNode(x); + dag.addNode(r); + dag.addNode(s); + dag.addNode(t); + dag.addNode(u); + dag.addNode(v); + dag.addNode(y); + + // Edges: x -> r -> s -> t <- u <- v -> y + dag.addDirectedEdge(x, r); + dag.addDirectedEdge(r, s); + dag.addDirectedEdge(s, t); + dag.addDirectedEdge(u, t); + dag.addDirectedEdge(v, u); + dag.addDirectedEdge(v, y); + + // Check d-separations + assertFalse(Utils.dSeparated(dag, x, r)); + assertFalse(Utils.dSeparated(dag, x, s)); + assertFalse(Utils.dSeparated(dag, x, t)); + + assertTrue(Utils.dSeparated(dag, x, u)); + assertTrue(Utils.dSeparated(dag, x, v)); + assertTrue(Utils.dSeparated(dag, x, y)); + + assertFalse(Utils.dSeparated(dag, u, v)); + assertFalse(Utils.dSeparated(dag, u, y)); + } + + public void testDseparationRule2(){ + // Scenarios taken from https://yuyangyy.medium.com/understand-d-separation-471f9aada503 + // Scenario 2: Z is non-empty, and the colliders don't belong to Z or have no children in Z. + // Rule 2: If there exists a path from x to y and none of the nodes on the path belongs to Z, then x and y are not d-separated. + Node x = new GraphNode("x"); + Node r = new GraphNode("r"); + Node s = new GraphNode("s"); + Node t = new GraphNode("t"); + Node u = new GraphNode("u"); + Node v = new GraphNode("v"); + Node y = new GraphNode("y"); + + Dag dag = new Dag(); + dag.addNode(x); + dag.addNode(r); + dag.addNode(s); + dag.addNode(t); + dag.addNode(u); + dag.addNode(v); + dag.addNode(y); + + // Edges: x -> r -> s -> t <- u <- v -> y + dag.addDirectedEdge(x, r); + dag.addDirectedEdge(r, s); + dag.addDirectedEdge(s, t); + dag.addDirectedEdge(u, t); + dag.addDirectedEdge(v, u); + dag.addDirectedEdge(v, y); + + // Creating a conditioning set Z that does not include any colliders + List Z = new ArrayList<>(); + Z.add(r); + Z.add(v); + + // Check d-separations + // Node x + assertTrue(Utils.dSeparated(dag, x, r, Z)); + assertTrue(Utils.dSeparated(dag, x, s, Z)); + assertTrue(Utils.dSeparated(dag, x, t, Z)); + assertTrue(Utils.dSeparated(dag, x, u, Z)); + assertTrue(Utils.dSeparated(dag, x, v, Z)); + assertTrue(Utils.dSeparated(dag, x, y, Z)); + + // Node r + assertTrue(Utils.dSeparated(dag, r, s, Z)); + assertTrue(Utils.dSeparated(dag, r, t, Z)); + assertTrue(Utils.dSeparated(dag, r, u, Z)); + assertTrue(Utils.dSeparated(dag, r, v, Z)); + assertTrue(Utils.dSeparated(dag, r, y, Z)); + + // Node s + assertFalse(Utils.dSeparated(dag, s, t, Z)); // No node on the path belongs to Z, so d-separated + assertTrue(Utils.dSeparated(dag, s, u, Z)); + assertTrue(Utils.dSeparated(dag, s, v, Z)); + assertTrue(Utils.dSeparated(dag, s, y, Z)); + + // Node t + assertFalse(Utils.dSeparated(dag, t, u, Z)); // No node on the path belongs to Z, so d-separated + assertTrue(Utils.dSeparated(dag, t, v, Z)); + assertTrue(Utils.dSeparated(dag, t, y, Z)); + + // Node u + assertTrue(Utils.dSeparated(dag, u, v, Z)); + assertTrue(Utils.dSeparated(dag, u, y, Z)); + + // Node v + assertTrue(Utils.dSeparated(dag, v, y, Z)); + + + } + + @Test + public void testDseparationRule3(){ + // Scenarios taken from https://yuyangyy.medium.com/understand-d-separation-471f9aada503 + // Scenario 3: Z is non-empty, and there are colliders either inside Z or have children in Z. + // Rule 3: For colliders that fall inside Z or have children in Z, they are no longer seen as colliders. + + Node x = new GraphNode("x"); + Node r = new GraphNode("r"); + Node s = new GraphNode("s"); + Node t = new GraphNode("t"); + Node u = new GraphNode("u"); + Node v = new GraphNode("v"); + Node y = new GraphNode("y"); + Node w = new GraphNode("w"); + Node p = new GraphNode("p"); + Node q = new GraphNode("q"); + + Dag dag = new Dag(); + dag.addNode(x); + dag.addNode(r); + dag.addNode(s); + dag.addNode(t); + dag.addNode(u); + dag.addNode(v); + dag.addNode(y); + dag.addNode(w); + dag.addNode(p); + dag.addNode(q); + + // Edges: x -> r -> s -> t <- u <- v -> y + r->w, t->p, v->q + dag.addDirectedEdge(x, r); + dag.addDirectedEdge(r, s); + dag.addDirectedEdge(s, t); + dag.addDirectedEdge(u, t); + dag.addDirectedEdge(v, u); + dag.addDirectedEdge(v, y); + dag.addDirectedEdge(r, w); + dag.addDirectedEdge(t, p); + dag.addDirectedEdge(v, q); + + // Creating a conditioning set Z that includes colliders and their children + List Z = new ArrayList<>(); + Z.add(r); + Z.add(p); + + // Check d-separations + // Node x + assertTrue(Utils.dSeparated(dag, x, s, Z)); + assertTrue(Utils.dSeparated(dag, x, t, Z)); + assertTrue(Utils.dSeparated(dag, x, u, Z)); + assertTrue(Utils.dSeparated(dag, x, v, Z)); + assertTrue(Utils.dSeparated(dag, x, y, Z)); + assertTrue(Utils.dSeparated(dag, x, w, Z)); + assertTrue(Utils.dSeparated(dag, x, q, Z)); + + // Node w + assertTrue(Utils.dSeparated(dag, w, s, Z)); + assertTrue(Utils.dSeparated(dag, w, t, Z)); + assertTrue(Utils.dSeparated(dag, w, u, Z)); + assertTrue(Utils.dSeparated(dag, w, v, Z)); + assertTrue(Utils.dSeparated(dag, w, y, Z)); + assertTrue(Utils.dSeparated(dag, w, q, Z)); + + // Node s + assertFalse(Utils.dSeparated(dag, s, t, Z)); + assertFalse(Utils.dSeparated(dag, s, u, Z)); + assertFalse(Utils.dSeparated(dag, s, v, Z)); + assertFalse(Utils.dSeparated(dag, s, q, Z)); + assertFalse(Utils.dSeparated(dag, s, y, Z)); + + // Node t + assertFalse(Utils.dSeparated(dag, t, u, Z)); + assertFalse(Utils.dSeparated(dag, t, v, Z)); + assertFalse(Utils.dSeparated(dag, t, y, Z)); + assertFalse(Utils.dSeparated(dag, t, q, Z)); + + // Node u + assertFalse(Utils.dSeparated(dag, u, v, Z)); + assertFalse(Utils.dSeparated(dag, u, y, Z)); + assertFalse(Utils.dSeparated(dag, u, q, Z)); + + // Node v + assertFalse(Utils.dSeparated(dag, v, y, Z)); + assertFalse(Utils.dSeparated(dag, v, q, Z)); + + // Node y + assertFalse(Utils.dSeparated(dag, y, q, Z)); + + } +} diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/FindNaYXTest.java similarity index 53% rename from src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java rename to src/test/java/es/uclm/i3a/simd/consensusBN/FindNaYXTest.java index 50657cf..c386850 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/UtilsTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/FindNaYXTest.java @@ -1,6 +1,5 @@ package es.uclm.i3a.simd.consensusBN; -import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -8,120 +7,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; import edu.cmu.tetrad.graph.EdgeListGraph; -import edu.cmu.tetrad.graph.Edges; import edu.cmu.tetrad.graph.Endpoint; import edu.cmu.tetrad.graph.Graph; import edu.cmu.tetrad.graph.GraphNode; import edu.cmu.tetrad.graph.Node; -public class UtilsTest { - // d-separation tests - - private Node node(String name) { - return new GraphNode(name); - } - - private Dag createDag(Edge... edges) { - Dag dag = new Dag(); - for (Edge edge : edges) { - dag.addNode(edge.getNode1()); - dag.addNode(edge.getNode2()); - dag.addDirectedEdge(edge.getNode1(), edge.getNode2()); - } - return dag; - } - - @Test - public void testDirectConnection() { - Node A = node("A"), B = node("B"); - Dag dag = createDag(Edges.directedEdge(A, B)); - List conditioning = Collections.emptyList(); - assertFalse(Utils.dSeparated(dag, A, B, conditioning)); - } - - @Test - public void testChainNoCondition() { - Node A = node("A"), B = node("B"), C = node("C"); - Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(B, C)); - List conditioning = Collections.emptyList(); - assertFalse(Utils.dSeparated(dag, A, C, conditioning)); - } - - @Test - public void testChainWithCondition() { - Node A = node("A"), B = node("B"), C = node("C"); - Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(B, C)); - - assertTrue(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); - } - - @Test - public void testColliderNoCondition() { - Node A = node("A"), B = node("B"), C = node("C"); - Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, B)); - List conditioning = Collections.emptyList(); - assertTrue(Utils.dSeparated(dag, A, C, conditioning)); - } - - @Test - public void testColliderConditionedOnCollider() { - Node A = node("A"), B = node("B"), C = node("C"); - Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, B)); - - assertFalse(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); - } - - @Test - public void testDivergingNoCondition() { - Node A = node("A"), B = node("B"), C = node("C"); - Dag dag = createDag(Edges.directedEdge(B, A), Edges.directedEdge(B, C)); - List conditioning = Collections.emptyList(); - assertFalse(Utils.dSeparated(dag, A, C, conditioning)); - } - - @Test - public void testDivergingConditionedOnCommonParent() { - Node A = node("A"), B = node("B"), C = node("C"); - Dag dag = createDag(Edges.directedEdge(B, A), Edges.directedEdge(B, C)); - - assertTrue(Utils.dSeparated(dag, A, C, Collections.singletonList(B))); - } - - @Test - public void testColliderConditionedOnDescendant() { - Node A = node("A"), B = node("B"), C = node("C"), D = node("D"); - Dag dag = createDag( - Edges.directedEdge(A, B), - Edges.directedEdge(C, B), - Edges.directedEdge(B, D) - ); - - assertFalse(Utils.dSeparated(dag, A, C, Collections.singletonList(D))); - } - - // Asegurar que esto funciona correctamente!!!!!!! - @Test - public void testSimmetryBetweenXandY() { - Node A = node("A"), B = node("B"), C = node("C"), D = node("D"); - Dag dag = createDag(Edges.directedEdge(A, B), Edges.directedEdge(C, A), Edges.directedEdge(D, A)); - List conditioning = Collections.emptyList(); - - // No colliders between A and B, so they are not d-separated - assertFalse(Utils.dSeparated(dag, A, B, conditioning)); - assertFalse(Utils.dSeparated(dag, B, A, conditioning)); - - // C->A and D->A makes A a collider for C and D, and therefore C and D are d-separated from each other - assertTrue(Utils.dSeparated(dag, C, D, conditioning)); - assertTrue(Utils.dSeparated(dag, D, C, conditioning)); - - } - +public class FindNaYXTest { + //find naYX tests - @Test public void testFindNaYX_singleUndirectedCommonNeighbor() { Graph graph = new EdgeListGraph(); @@ -240,4 +136,5 @@ public void testFindNaYX_noCommonNeighbors() { List result = Utils.findNaYX(x, y, graph); assertTrue(result.isEmpty()); } + } From 9966f77fe9bbb311f6cfc7acfb97f2856e62f82a Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:47:38 +0200 Subject: [PATCH 14/32] Cleaning and Testing PairWiseConsensusBES --- .../i3a/simd/consensusBN/ConsensusBES.java | 4 +- ...HierarchicalAgglomerativeClustererBNs.java | 10 +- .../consensusBN/PairWiseConsensusBES.java | 148 +++++++++++++++--- .../simd/consensusBN/ConsensusBESTest.java | 6 +- .../consensusBN/PairWiseConsensusBESTest.java | 105 +++++++++++++ 5 files changed, 237 insertions(+), 36 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBESTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java index c893d85..83e7ab0 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java @@ -105,7 +105,7 @@ public void fusion(){ * This method retrieves the final fused DAG, which represents the optimal fusion of the input DAGs. * @return the resulting output DAG after the fusion process. */ - public Dag getFusion(){ + public Dag getFusionDag(){ return this.outputDag; } @@ -114,7 +114,7 @@ public Dag getFusion(){ * @return */ public List getOrderFusion(){ - return this.getFusion().paths().getValidOrder(this.getFusion().getNodes(),true); + return this.getFusionDag().paths().getValidOrder(this.getFusionDag().getNodes(),true); } /** diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNs.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNs.java index 1965f28..dee7bb2 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNs.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNs.java @@ -179,7 +179,7 @@ public Dag computeConsensusDag(int level){ for(int j = 0; j< this.setOfBNs.size(); j++){ for(int k = 0; k< this.setOfBNs.size(); k++){ if(this.clustersIndexes[cluster][j][level]&&this.clustersIndexes[cluster][k][level]&&(j!=k)){ - distance[j]+=this.initialpairwisedistance[j][k].getHammingDistance();//getNumberOfInsertedEdges(); + distance[j]+=this.initialpairwisedistance[j][k].calculateHammingDistance();//getNumberOfInsertedEdges(); } } if(clustersIndexes[cluster][j][level]&&distance[j] 0 && (this.maxSize >= (this.clusterCardinalities[o1] + this.clusterCardinalities[o2]))|| level == 0){ PairWiseConsensusBES pairBNs = new PairWiseConsensusBES(this.clustersBN[o1][level],this.clustersBN[o2][level]); PairWiseConsensusBES pairDag= (PairWiseConsensusBES) pairBNs; - pairDag.getFusion(); + pairDag.fusion(); return pairBNs; }else if(this.maxSize == 0){ PairWiseConsensusBES pairBNs = new PairWiseConsensusBES(this.clustersBN[o1][level],this.clustersBN[o2][level]); PairWiseConsensusBES pairDag= (PairWiseConsensusBES) pairBNs; - pairDag.getFusion(); + pairDag.fusion(); if((pairDag.getDagFusion().getNumEdges())/this.averageNEdges <= this.maxComplexityCluster|| level == 0) return pairBNs; else return null; @@ -326,7 +326,7 @@ private Pair findMostSimilarClusters() { PairWiseConsensusBES inCluster = dissimilarityMatrix[cluster][neighbor]; if(inCluster!= null){ double complexity = 0.0; - complexity = (float) inCluster.getHammingDistance();//getNumberOfInsertedEdges(); + complexity = (float) inCluster.calculateHammingDistance();//getNumberOfInsertedEdges(); if (indexUsed[neighbor]&&complexity setOfDags = new ArrayList(); - setOfDags.add(this.b1); - setOfDags.add(this.b2); - conBES = new ConsensusBES(setOfDags); - conBES.fusion(); - this.numberOfInsertedEdges = conBES.getNumberOfInsertedEdges(); - this.numberOfUnionEdges = conBES.getUnion().getNumEdges(); - this.conDAG = conBES.getFusion(); + /** + * Checks if the input DAGs are valid. + * Validity is determined by ensuring that the DAGs are not null, contain at least one node and one edge, and have the same set of nodes. + * If any of these conditions are not met, an IllegalArgumentException is thrown. + * @param firstDag first input DAG + * @param secondDag second input DAG + * @throws IllegalArgumentException if the input DAGs are not valid + */ + private void checkInput(Dag firstDag, Dag secondDag) { + if (firstDag == null || secondDag == null) { + throw new IllegalArgumentException("Input DAGs cannot be null."); + } + if (firstDag.getNumNodes() == 0 || secondDag.getNumNodes() == 0) { + throw new IllegalArgumentException("Input DAGs must contain at least one node."); + } + if (firstDag.getNumEdges() == 0 || secondDag.getNumEdges() == 0) { + throw new IllegalArgumentException("Input DAGs must contain at least one edge."); + } + if (firstDag.getNodes().size() != secondDag.getNodes().size()) { + throw new IllegalArgumentException("Input DAGs must have the same number of nodes."); + } + if (!firstDag.getNodes().containsAll(secondDag.getNodes())) { + throw new IllegalArgumentException("Input DAGs must have the same set of nodes."); + } } - + + /** + * Performs the fusion process by first applying the consensus union and then applying the Backward Equivalence Search. + */ + public void fusion(){ + // Creating a list of DAGs to be fused + ArrayList setOfDags = new ArrayList<>(); + setOfDags.add(this.firstDag); + setOfDags.add(this.secondDag); + // Applying the ConsensusBES algorithm to fuse the DAGs + consensusBES = new ConsensusBES(setOfDags); + consensusBES.fusion(); + // Retrieving the resulting DAG and the number of inserted edges + this.numberOfInsertedEdges = consensusBES.getNumberOfInsertedEdges(); + this.numberOfUnionEdges = consensusBES.getUnion().getNumEdges(); + this.consensusDAG = consensusBES.getFusionDag(); + } + + /** + * Returns the number of edges inserted during the fusion process. + * This method retrieves the number of edges that were added to the consensus DAG during the fusion process. + * It is useful for understanding how many edges were introduced in the consensus DAG compared to the original input DAGs. + * @return + */ public int getNumberOfInsertedEdges(){ return this.numberOfInsertedEdges; } + /** + * Returns the number of edges in the union DAG after the consensus union process. + * This method retrieves the number of edges that were present in the union DAG after merging the transformed input DAGs. + * It is useful for understanding the size of the union DAG before applying the Backward Equivalence Search. + * This number can be used to compare with the number of edges in the final consensus DAG after the Backward Equivalence Search. + * + * @see ConsensusBES#getUnion() + * @see ConsensusBES#getNumberOfInsertedEdges() + * @return + */ public int getNumberOfUnionEdges(){ return this.numberOfUnionEdges; } - public int getHammingDistance(){ - if(this.conDAG==null) this.getFusion(); + /** + * Calculates the Hamming distance between the optimum fusion DAG and the original input DAGs. + * @return The Hamming distance between the fused DAG and the original input DAGs. + */ + public int calculateHammingDistance(){ + if(this.consensusDAG==null) this.fusion(); int distance = 0; - for(Edge ed: this.conDAG.getEdges()){ + for(Edge ed: this.consensusDAG.getEdges()){ Node tail = ed.getNode1(); Node head = ed.getNode2(); - for(Dag g: conBES.getTransformedDags()){ + for(Dag g: consensusBES.getTransformedDags()){ Edge edge1 = g.getEdge(tail, head); Edge edge2 = g.getEdge(head, tail); if(edge1 == null && edge2==null) distance++; @@ -57,14 +142,25 @@ public int getHammingDistance(){ return distance+this.getNumberOfInsertedEdges(); } + /** + * Returns the resulting consensus DAG after applying the fusion process. + * This method retrieves the final fused DAG, which represents the optimal fusion of the input DAGs. + * It is useful for obtaining the consensus structure after the fusion process has been completed. + * + * @see ConsensusBES#getFusionDag() + * @return + */ public Dag getDagFusion(){ - return this.conDAG; + return this.consensusDAG; } + /** + * Runs the fusion process in a thread, performing the consensus union and the Backward Equivalence Search with D-separation. + */ @Override public void run() { - this.getFusion(); + this.fusion(); } } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java index da9a321..528b4d8 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java @@ -111,7 +111,7 @@ public void testRandomBNFusion(){ ConsensusBES conDag = new ConsensusBES(setOfDags.setOfRandomDags); conDag.fusion(); - Dag besDag = conDag.getFusion(); + Dag besDag = conDag.getFusionDag(); Dag unionDag = conDag.getUnion(); ConsensusUnion consensusUnion = conDag.getConsensusUnion(); int totalNumberOfInsertedEdges = conDag.getNumberOfInsertedEdges(); @@ -131,7 +131,7 @@ void testFusionProducesDag() { ConsensusBES fusionAlgorithm = new ConsensusBES(inputDags); fusionAlgorithm.fusion(); - Dag result = fusionAlgorithm.getFusion(); + Dag result = fusionAlgorithm.getFusionDag(); assertNotNull(result, "El DAG de salida no debe ser null."); assertFalse(result.paths().existsDirectedCycle(), "El DAG resultante no debe tener ciclos."); } @@ -183,7 +183,7 @@ void testThreadExecutionWithRunMethod() { fail("El hilo fue interrumpido."); } - assertNotNull(fusionAlgorithm.getFusion(), "El DAG resultante debe existir tras ejecutar run()."); + assertNotNull(fusionAlgorithm.getFusionDag(), "El DAG resultante debe existir tras ejecutar run()."); } } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBESTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBESTest.java new file mode 100644 index 0000000..0c4c1cd --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBESTest.java @@ -0,0 +1,105 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.Edge; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +public class PairWiseConsensusBESTest { + + private Node A, B, C; + private Dag dag1, dag2; + + @BeforeEach + public void setup() { + A = new GraphNode("A"); + B = new GraphNode("B"); + C = new GraphNode("C"); + + dag1 = new Dag(); + dag1.addNode(A); + dag1.addNode(B); + dag1.addNode(C); + dag1.addDirectedEdge(A, B); + dag1.addDirectedEdge(B, C); + + dag2 = new Dag(); + dag2.addNode(A); + dag2.addNode(B); + dag2.addNode(C); + dag2.addDirectedEdge(A, C); + dag2.addDirectedEdge(C, B); + } + + @Test + public void testFusionCreatesNonNullDag() { + PairWiseConsensusBES pwc = new PairWiseConsensusBES(dag1, dag2); + pwc.fusion(); + + Dag fusion = pwc.getDagFusion(); + assertNotNull(fusion, "The fusion DAG should not be null"); + } + + @Test + public void testGetNumberOfInsertedEdges() { + PairWiseConsensusBES pwc = new PairWiseConsensusBES(dag1, dag2); + pwc.fusion(); + + int inserted = pwc.getNumberOfInsertedEdges(); + assertTrue(inserted >= 0, "Inserted edges should be >= 0"); + } + + @Test + public void testGetNumberOfUnionEdges() { + PairWiseConsensusBES pwc = new PairWiseConsensusBES(dag1, dag2); + pwc.fusion(); + + PairWiseConsensusBES samePwc = new PairWiseConsensusBES(dag1, dag1); + samePwc.fusion(); + + int unionEdges = pwc.getNumberOfUnionEdges(); + assertTrue(unionEdges > 0, "Union should contain some edges"); + + int sameUnionEdges = samePwc.getNumberOfUnionEdges(); + assertTrue(sameUnionEdges == dag1.getNumEdges(), "Union edges should match the number of edges in a single DAG"); + } + + @Test + public void testGetHammingDistance() { + PairWiseConsensusBES pwc = new PairWiseConsensusBES(dag1, dag2); + int distance = pwc.calculateHammingDistance(); + PairWiseConsensusBES samePwc = new PairWiseConsensusBES(dag1, dag1); + int sameDistance = samePwc.calculateHammingDistance(); + + assertTrue(distance >= 0, "Hamming distance should be >= 0"); + assertTrue(sameDistance == 0, "Hamming distance for identical DAGs should be 0"); + } + + @Test + public void testRunCallsGetFusion() { + PairWiseConsensusBES pwc = new PairWiseConsensusBES(dag1, dag2); + pwc.run(); + + assertNotNull(pwc.getDagFusion(), "Fusion DAG should be created after run()"); + assertTrue(pwc.getNumberOfUnionEdges() > 0, "Union edges should be computed"); + } + + @Test + public void testFusionIsConsistentWithInput() { + PairWiseConsensusBES pwc = new PairWiseConsensusBES(dag1, dag2); + pwc.fusion(); + Dag fusion = pwc.getDagFusion(); + + Set edges = fusion.getEdges(); + assertNotNull(edges); + assertFalse(edges.isEmpty(), "Fusion DAG should contain edges"); + } +} From a5626e41b963db04c668834e6a5463fdeb98e146 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:53:56 +0200 Subject: [PATCH 15/32] Improving coverage of BetaToAlpha --- .../i3a/simd/consensusBN/BetaToAlphaTest.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java index 7d421ad..06504f7 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java @@ -1,9 +1,9 @@ package es.uclm.i3a.simd.consensusBN; import java.util.ArrayList; -import java.util.List; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Random; import java.util.Set; @@ -90,4 +90,20 @@ void testComputeAlphaHashBuildsCorrectMap() { assertEquals(i, (bta.getAlphaHash()).get(node)); } } + + + @Test + public void setterAndGetterTest(){ + ArrayList firstOrder = new ArrayList<>(Arrays.asList(a, b, c)); + BetaToAlpha b2Alpha = new BetaToAlpha(dag, firstOrder); + List newOrder = new ArrayList<>(Arrays.asList(c, b, a)); + b2Alpha.setAlphaOrder(newOrder); + + List order = b2Alpha.getAlphaOrder(); + assertNotNull(order); + assertEquals(3, order.size()); + assertTrue(order.contains(c)); + assertTrue(order.contains(b)); + assertTrue(order.contains(a)); + } } From 15220698bcf606f4ca106b6f5dc43b82c404fcdb Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:31:50 +0200 Subject: [PATCH 16/32] Removing SubSet for HashSet --- .../BackwardEquivalenceSearchDSep.java | 10 +++--- .../consensusBN/HeuristicConsensusBES.java | 4 +-- .../uclm/i3a/simd/consensusBN/PowerSet.java | 35 ++++++++++--------- .../es/uclm/i3a/simd/consensusBN/SubSet.java | 24 ------------- 4 files changed, 25 insertions(+), 48 deletions(-) delete mode 100644 src/main/java/es/uclm/i3a/simd/consensusBN/SubSet.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index 4b3334f..1764b8b 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -128,7 +128,7 @@ public Dag applyBackwardEliminationWithDSeparation(){ PowerSet hSubsets= PowerSetFabric.getPowerSet(candidateTail,candidateHead,hNeighbors); while(hSubsets.hasMoreElements()) { - SubSet hSubset=hSubsets.nextElement(); + HashSet hSubset=hSubsets.nextElement(); // Checking if {naYXH} \ {hSubset} is a clique List naYXH = findNaYX(candidateTail, candidateHead, graph); @@ -226,8 +226,8 @@ private EdgeCandidate calculateBestCandidateEdge(List edges, double score) PowerSet hSubsets= PowerSetFabric.getPowerSet(candidateTail,candidateHead,hNeighbors); while(hSubsets.hasMoreElements()) { - // Getting a subset of hNeighbors - SubSet hSubset=hSubsets.nextElement(); + // Getting a HashSet of hNeighbors + HashSet hSubset=hSubsets.nextElement(); // Checking if {naYXH} \ {hSubset} is a clique List naYXH = Utils.findNaYX(candidateTail, candidateHead, graph); @@ -374,7 +374,7 @@ private static List getHNeighbors(Node x, Node y, Graph graph) { /** * Applies the delete operation from Chickering 2002 for the edge x->y in the graph, and updates the edges - * connecting x and y to the nodes in the provided subset. This is done to ensure that the same dependency structure is maintained + * connecting x and y to the nodes in the provided HashSet. This is done to ensure that the same dependency structure is maintained * while removing the edge between x and y. * @param tailNode The tail node of the edge to be deleted. * @param headNode The head node of the edge to be deleted. @@ -408,7 +408,7 @@ private static void delete(Node tailNode, Node headNode, Set subset, Graph * @param graph The graph in which the change is being evaluated. * @return The score resulting from deleting the edge, based on the given context. */ - private double deleteEval(Node x, Node y, SubSet conditioningSet, Graph graph){ + private double deleteEval(Node x, Node y, HashSet conditioningSet, Graph graph){ // Setup the conditioning set for d-separation by removing the conditioning nodes from the naYX set, adding the parents of y and removing x. Set finalConditioningSet = new HashSet<>(Utils.findNaYX(x, y, graph)); finalConditioningSet.removeAll(conditioningSet); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java index 8d2a496..cbf672f 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java @@ -115,7 +115,7 @@ public void fusion(){ // List> hSubsets = powerSet(hNeighbors); PowerSet hSubsets= PowerSetFabric.getPowerSet(_x,_y,hNeighbors); while(hSubsets.hasMoreElements()) { - SubSet hSubset=hSubsets.nextElement(); + HashSet hSubset=hSubsets.nextElement(); if(hSubset.size() > maxSize) break; double deleteEval = deleteEval(_x, _y, hSubset, graph); if (!(deleteEval >= this.percentage)) deleteEval = 0.0; @@ -248,7 +248,7 @@ private static List getHNeighbors(Node x, Node y, Graph graph) { } - double deleteEval(Node x, Node y, SubSet h, Graph graph){ + double deleteEval(Node x, Node y, HashSet h, Graph graph){ Set set1 = new HashSet(Utils.findNaYX(x, y, graph)); set1.removeAll(h); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java index ebfe47d..cfe55ff 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java @@ -3,29 +3,30 @@ import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import edu.cmu.tetrad.graph.Node; -public class PowerSet implements Enumeration { +public class PowerSet implements Enumeration> { List nodes; - private List subSets; + private List> subSets; private int index; private int[] lista; - private HashMap hashMap; + private HashMap> hashMap; PowerSet(List nodes,int k) { if(nodes.size()<=k) k=nodes.size(); this.nodes=nodes; - subSets = new ArrayList(); + subSets = new ArrayList>(); index=0; - hashMap=new HashMap(); + hashMap=new HashMap>(); lista=ListFabric.getList(nodes.size()); for (int i : lista) { - SubSet newSubSet = new SubSet(); + HashSet newSubSet = new HashSet(); String selection = Integer.toBinaryString(i); for (int j = selection.length() - 1; j >= 0; j--) { if (selection.charAt(j) == '1') { @@ -44,12 +45,12 @@ public class PowerSet implements Enumeration { if(nodes.size()>maxPow) maxPow=nodes.size(); this.nodes=nodes; - subSets = new ArrayList(); + subSets = new ArrayList>(); index=0; - hashMap=new HashMap(); + hashMap=new HashMap>(); lista=ListFabric.getList(nodes.size()); for (int i : lista) { - SubSet newSubSet = new SubSet(); + HashSet newSubSet = new HashSet(); String selection = Integer.toBinaryString(i); for (int j = selection.length() - 1; j >= 0; j--) { if (selection.charAt(j) == '1') { @@ -65,7 +66,7 @@ public boolean hasMoreElements() { return index nextElement() { return subSets.get(index++); } @@ -83,9 +84,9 @@ public static long maxPowerSetSize() { // for(int i=0;i.TEST_TRUE; // else -// hashMap.get(lista[i]).firstTest=SubSet.TEST_FALSE; +// hashMap.get(lista[i]).firstTest=HashSet.TEST_FALSE; // } // } // } @@ -95,9 +96,9 @@ public static long maxPowerSetSize() { // for(int i=0;i.TEST_TRUE; // else -// hashMap.get(lista[i]).secondTest=SubSet.TEST_FALSE; +// hashMap.get(lista[i]).secondTest=HashSet.TEST_FALSE; // } // } // } @@ -105,11 +106,11 @@ public static long maxPowerSetSize() { // public void reset(boolean isFordwardSearch) { // index=0; // for(int i=0;i aux=subSets.get(i); // if(isFordwardSearch) -// aux.secondTest=SubSet.TEST_NOT_EVALUATED; +// aux.secondTest=HashSet.TEST_NOT_EVALUATED; // else -// aux.firstTest=SubSet.TEST_NOT_EVALUATED; +// aux.firstTest=HashSet.TEST_NOT_EVALUATED; // } // } } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/SubSet.java b/src/main/java/es/uclm/i3a/simd/consensusBN/SubSet.java deleted file mode 100644 index be6018c..0000000 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/SubSet.java +++ /dev/null @@ -1,24 +0,0 @@ -package es.uclm.i3a.simd.consensusBN; - -import java.util.HashSet; - -import edu.cmu.tetrad.graph.Node; - -public class SubSet extends HashSet { - - private static final long serialVersionUID = 4569314863278L; - public static final int TEST_NOT_EVALUATED=0; - public static final int TEST_TRUE=1; - public static final int TEST_FALSE=-1; - - public int firstTest=TEST_NOT_EVALUATED; - public int secondTest=TEST_NOT_EVALUATED; - - public SubSet() { - super(); - } - - public SubSet(SubSet other) { - super(other); - } -} \ No newline at end of file From 8f8974b0236a4cfe502ecd65f4a1206a6f5a93fd Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:42:48 +0200 Subject: [PATCH 17/32] Improving coverage of TransformDags --- .../simd/consensusBN/TransformDagsTest.java | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/TransformDagsTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/TransformDagsTest.java index b399ed3..86cb1d6 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/TransformDagsTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/TransformDagsTest.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; @@ -67,11 +68,42 @@ public void setUp() { } @Test - public void testConstructorInitializesCorrectly() { + public void testConstructorGettersAndSetters() { TransformDags transformer = new TransformDags(inputDags, alpha); - + ArrayList empytBetas = new ArrayList<>(); + assertNotNull(transformer); assertEquals(0, transformer.getNumberOfInsertedEdges()); + + // GetSetOfOutputDags + ArrayList outputDags = transformer.getSetOfOutputDags(); + assertNotNull(outputDags); + assertTrue(outputDags.isEmpty()); + transformer.transform(); + outputDags = transformer.getSetOfOutputDags(); + assertNotNull(outputDags); + assertEquals(inputDags.size(), outputDags.size()); + assertNotEquals(inputDags, outputDags); + + // Testing setTransformers and getTransformers + ArrayList betas = transformer.getTransformers(); + assertNotNull(betas); + assertTrue(!betas.isEmpty()); + transformer.setTransformers(empytBetas); + assertEquals(empytBetas, transformer.getTransformers()); + assertTrue(transformer.getTransformers().isEmpty()); + + // GetAlphaOrder + ArrayList alphaOrder = transformer.getAlpha(); + assertNotNull(alphaOrder); + assertEquals(alpha, alphaOrder); + + // GetSetOfDags + ArrayList dags = transformer.getSetOfDags(); + assertNotNull(dags); + assertEquals(inputDags, dags); + + } @Test @@ -101,4 +133,5 @@ public void testEmptyDagListReturnsEmptyOutput() { assertTrue(result.isEmpty()); assertEquals(0, transformer.getNumberOfInsertedEdges()); } + } From 7768c375116c974afdea47b394da222b03e15559 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:32:40 +0200 Subject: [PATCH 18/32] Cleaning and Testing PowerSet, ListFabric and deleting PowerSetFabric --- .../BackwardEquivalenceSearchDSep.java | 12 +- .../consensusBN/HeuristicConsensusBES.java | 6 +- .../uclm/i3a/simd/consensusBN/ListFabric.java | 50 +++-- .../uclm/i3a/simd/consensusBN/PowerSet.java | 177 +++++++++--------- .../i3a/simd/consensusBN/PowerSetFabric.java | 71 ------- .../i3a/simd/consensusBN/ListFabricTest.java | 70 +++++++ .../i3a/simd/consensusBN/PowerSetTest.java | 89 +++++++++ 7 files changed, 283 insertions(+), 192 deletions(-) delete mode 100644 src/main/java/es/uclm/i3a/simd/consensusBN/PowerSetFabric.java create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/ListFabricTest.java create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/PowerSetTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index 1764b8b..9415958 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -223,11 +223,11 @@ private EdgeCandidate calculateBestCandidateEdge(List edges, double score) Node candidateHead = Edges.getDirectedEdgeHead(edge); List hNeighbors = getHNeighbors(candidateTail, candidateHead, graph); - PowerSet hSubsets= PowerSetFabric.getPowerSet(candidateTail,candidateHead,hNeighbors); + PowerSet hSubsets= new PowerSet(hNeighbors);//PowerSetFabric.getPowerSet(candidateTail,candidateHead,hNeighbors); while(hSubsets.hasMoreElements()) { // Getting a HashSet of hNeighbors - HashSet hSubset=hSubsets.nextElement(); + Set hSubset=hSubsets.nextElement(); // Checking if {naYXH} \ {hSubset} is a clique List naYXH = Utils.findNaYX(candidateTail, candidateHead, graph); @@ -279,9 +279,9 @@ private double executeEdgeDeletion(EdgeCandidate bestCandidate) { bestScore = bestCandidate.score; // Applying delete - System.out.println(" "); - System.out.println("DELETE " + graph.getEdge(bestTail, bestHead) + bestSetParents.toString() + " (" +bestScore + ")"); - System.out.println(" "); + //System.out.println(" "); + //System.out.println("DELETE " + graph.getEdge(bestTail, bestHead) + bestSetParents.toString() + " (" +bestScore + ")"); + //System.out.println(" "); delete(bestTail, bestHead, bestSetParents, graph); // Rebuilding the pattern after deleting the edge @@ -408,7 +408,7 @@ private static void delete(Node tailNode, Node headNode, Set subset, Graph * @param graph The graph in which the change is being evaluated. * @return The score resulting from deleting the edge, based on the given context. */ - private double deleteEval(Node x, Node y, HashSet conditioningSet, Graph graph){ + private double deleteEval(Node x, Node y, Set conditioningSet, Graph graph){ // Setup the conditioning set for d-separation by removing the conditioning nodes from the naYX set, adding the parents of y and removing x. Set finalConditioningSet = new HashSet<>(Utils.findNaYX(x, y, graph)); finalConditioningSet.removeAll(conditioningSet); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java index cbf672f..ae927d2 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java @@ -113,9 +113,9 @@ public void fusion(){ List hNeighbors = getHNeighbors(_x, _y, graph); // List> hSubsets = powerSet(hNeighbors); - PowerSet hSubsets= PowerSetFabric.getPowerSet(_x,_y,hNeighbors); + PowerSet hSubsets= new PowerSet(hNeighbors);//PowerSetFabric.getPowerSet(_x,_y,hNeighbors); while(hSubsets.hasMoreElements()) { - HashSet hSubset=hSubsets.nextElement(); + Set hSubset=hSubsets.nextElement(); if(hSubset.size() > maxSize) break; double deleteEval = deleteEval(_x, _y, hSubset, graph); if (!(deleteEval >= this.percentage)) deleteEval = 0.0; @@ -248,7 +248,7 @@ private static List getHNeighbors(Node x, Node y, Graph graph) { } - double deleteEval(Node x, Node y, HashSet h, Graph graph){ + double deleteEval(Node x, Node y, Set h, Graph graph){ Set set1 = new HashSet(Utils.findNaYX(x, y, graph)); set1.removeAll(h); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ListFabric.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ListFabric.java index cef982a..558a4bb 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ListFabric.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ListFabric.java @@ -1,41 +1,46 @@ package es.uclm.i3a.simd.consensusBN; +/** + * ListFabric is a utility class that generates lists of integers representing subsets + * of a set of a given size, with a maximum size constraint on the number of elements + * in each subset. + */ public class ListFabric { - private static int maxSize=Integer.MAX_VALUE; // Número máximo de unos que tienen los elementos de la lista. -// private static HashMap hashMap=new HashMap(); -// -// public static int[] getList(int size) { -// Integer key=size; -// int[] lista=hashMap.get(key); -// if(lista==null) { -// lista=generateList(size); -// hashMap.put(key, lista); -// } -// return lista; -// } - - public static int[] getList(int size) { - return generateList(size); - } - - private static int[] generateList(int size) { + /** + * MAX_SIZE is the maximum number of elements that can be included in any subset. + * It is set to Integer.MAX_VALUE by default, meaning no limit unless specified. + */ + public static int MAX_SIZE=Integer.MAX_VALUE; + + /** + * Generates a list of integers representing all subsets of a set of a given size. + * Each integer is a bitmask where the i-th bit represents the inclusion of the i-th element. + * @param size the size of the set for which subsets are generated + * @return an array of integers representing the subsets + */ + public static int[] generateList(int size) { int[] lista; if(size==0) { return new int[1]; } + // Generation of powers of numbers that are powers of 2 int[] pow2=new int[size]; pow2[0]=1; for(int i=1;iaux[0][index]) { aux[0][counter]=aux[0][index]+pow2[i]; @@ -50,11 +55,4 @@ private static int[] generateList(int size) { return lista; } - public static void setMaxSize(int maxParents) { - ListFabric.maxSize = maxParents; - } - - public static int getMaxSize() { - return ListFabric.maxSize; - } } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java index cfe55ff..6022718 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java @@ -5,112 +5,117 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; - +import java.util.Set; import edu.cmu.tetrad.graph.Node; -public class PowerSet implements Enumeration> { +/** + * PowerSet generates all subsets of a given set of nodes, with an optional maximum size constraint. + * It implements Enumeration to allow iteration over the subsets. + */ +public class PowerSet implements Enumeration> { + List nodes; - private List> subSets; + private final List> subSets = new ArrayList<>(); private int index; - private int[] lista; - private HashMap> hashMap; - + private int[] list; + private HashMap> subset; + private static int maxPow = 0; - PowerSet(List nodes,int k) { - if(nodes.size()<=k) - k=nodes.size(); - this.nodes=nodes; - subSets = new ArrayList>(); - index=0; - hashMap=new HashMap>(); - lista=ListFabric.getList(nodes.size()); - for (int i : lista) { - HashSet newSubSet = new HashSet(); - String selection = Integer.toBinaryString(i); - for (int j = selection.length() - 1; j >= 0; j--) { - if (selection.charAt(j) == '1') { - newSubSet.add(nodes.get(selection.length() - j - 1)); - } - } - if(newSubSet.size()<=k){ - subSets.add(newSubSet); - hashMap.put(i, newSubSet); - } + /** + * Builds a PowerSet with subsets of the given nodes, limited to a maximum size. + * @param nodes List of nodes to generate subsets from. + * @param maxSize Maximum size of the subsets to be generated. Assuring that k does not exceed the number of nodes. + * * If maxSize is negative, it will throw an IllegalArgumentException. + * @throws IllegalArgumentException if maxSize is negative. + + */ + public PowerSet(List nodes, int maxSize) { + if (maxSize < 0) { + throw new IllegalArgumentException("maxSize cannot be negative"); } - } + if (nodes.size() <= maxSize) { + maxSize = nodes.size(); + } + this.nodes = nodes; + initializeSubsets(maxSize); + } - - PowerSet(List nodes) { - if(nodes.size()>maxPow) - maxPow=nodes.size(); - this.nodes=nodes; - subSets = new ArrayList>(); - index=0; - hashMap=new HashMap>(); - lista=ListFabric.getList(nodes.size()); - for (int i : lista) { - HashSet newSubSet = new HashSet(); - String selection = Integer.toBinaryString(i); - for (int j = selection.length() - 1; j >= 0; j--) { - if (selection.charAt(j) == '1') { - newSubSet.add(nodes.get(selection.length() - j - 1)); - } - } - subSets.add(newSubSet); - hashMap.put(i, newSubSet); - } - } - + /** + * Builds a PowerSet with all subsets of the given nodes, without size limitation. + * + * @param nodes Lista de nodos de entrada. + */ + public PowerSet(List nodes) { + if (nodes.size() > maxPow) { + maxPow = nodes.size(); + } + this.nodes = nodes; + initializeSubsets(nodes.size()); // sin límite: k = nodes.size() + } + + /** + * Initializes the subsets based on the nodes and the maximum size. + * This method generates all possible subsets of the nodes, respecting the maximum size constraint. + * @param maxSize Maximum size of the subsets to be generated. + * If maxSize is greater than the number of nodes, it will generate subsets of all sizes. + * If maxSize is 0, it will only generate the empty set. + */ + private void initializeSubsets(int maxSize) { + subset = new HashMap<>(); + index = 0; + list = ListFabric.generateList(nodes.size()); + + for (int i : list) { + Set newSubSet = new HashSet<>(); + String selection = Integer.toBinaryString(i); + + for (int j = selection.length() - 1; j >= 0; j--) { + if (selection.charAt(j) == '1') { + int idx = selection.length() - j - 1; + newSubSet.add(nodes.get(idx)); + } + } + + if (newSubSet.size() <= maxSize) { + subSets.add(newSubSet); + subset.put(i, newSubSet); + } + } + } + + /** + * Checks if there are more subsets to iterate over. + * @return true if there are more subsets, false otherwise. + */ + @Override public boolean hasMoreElements() { return index nextElement() { + /** + * Returns the next subset in the enumeration. + * @return The next subset as a Set. + */ + @Override + public Set nextElement() { return subSets.get(index++); } + /** + * Resets the index to allow re-iteration over the subsets. + * This method allows the enumeration to start over from the beginning. + */ public void resetIndex(){ this.index = 0; } - private static int maxPow = 0; + /** + * Returns the maximum size of the power set based on the maximum number of nodes. + * This method calculates the size of the power set as 2 raised to the power of the maximum number of nodes. + * @return The maximum size of the power set. + */ public static long maxPowerSetSize() { return (long) Math.pow(2,maxPow); } - -// public void firstTest(boolean result) { -// int numInicial=lista[index-1]; -// for(int i=0;i.TEST_TRUE; -// else -// hashMap.get(lista[i]).firstTest=HashSet.TEST_FALSE; -// } -// } -// } -// -// public void secondTest(boolean result) { -// int numInicial=lista[index-1]; -// for(int i=0;i.TEST_TRUE; -// else -// hashMap.get(lista[i]).secondTest=HashSet.TEST_FALSE; -// } -// } -// } -// -// public void reset(boolean isFordwardSearch) { -// index=0; -// for(int i=0;i aux=subSets.get(i); -// if(isFordwardSearch) -// aux.secondTest=HashSet.TEST_NOT_EVALUATED; -// else -// aux.firstTest=HashSet.TEST_NOT_EVALUATED; -// } -// } } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSetFabric.java b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSetFabric.java deleted file mode 100644 index 6f57b62..0000000 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSetFabric.java +++ /dev/null @@ -1,71 +0,0 @@ -package es.uclm.i3a.simd.consensusBN; - -import java.util.List; - -import edu.cmu.tetrad.graph.Node; - -public class PowerSetFabric { - - public static final int MODE_FES=0; - public static final int MODE_BES=1; - - //private static int mode=MODE_FES; - private static boolean usePowerSetsCache=false; -// -// private static HashMap> hashMap=new HashMap>(); -// -// private PowerSetFabric() { -// } - - public static PowerSet getPowerSet(List nodes, int k){ - return new PowerSet(nodes,k); - - } - - public static PowerSet getPowerSet(Node x, Node y, List nodes) { - return new PowerSet(nodes); -// if(!usePowerSetsCache) -// return new PowerSet(nodes); -// PowerSet pSet=get(x,y); -// if(pSet==null || pSet.nodes.size()!=nodes.size()) { // if(pSet==null || !pSet.t.equals(nodes)) { -// pSet=new PowerSet(nodes); -// put(x,y,pSet); -// } -// else { -// pSet.reset(mode==MODE_FES); -// } -// return pSet; - } -// -// private static void put(Node x, Node y, PowerSet pSet) { -// hashMap.get(x).put(y, pSet); -// -// } -// -// private static PowerSet get(Node x, Node y) { -// HashMap aux=hashMap.get(x); -// if(aux==null) { -// aux=new HashMap(); -// hashMap.put(x, aux); -// } -// return aux.get(y); -// } -// -// public static int getMode() { -// return mode; -// } -// - /* - public static void setMode(int mode) { - PowerSetFabric.mode=mode; - } - */ - - public static boolean isUsePowerSetsCache() { - return usePowerSetsCache; - } - - public static void setUsePowerSetsCache(boolean usePowerSetsCache) { - PowerSetFabric.usePowerSetsCache = usePowerSetsCache; - } -} diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/ListFabricTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/ListFabricTest.java new file mode 100644 index 0000000..5a7744f --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/ListFabricTest.java @@ -0,0 +1,70 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ListFabricTest { + + @BeforeEach + public void setUp() { + // Aseguramos el valor por defecto del maxSize antes de cada test + ListFabric.MAX_SIZE = 2; + } + + @Test + public void testGetList_size3_maxSize2() { + List result = Arrays.stream(ListFabric.generateList(3)).boxed().collect(Collectors.toList()); + List expected = Arrays.asList( + 0b000, // [] + 0b001, // [C] + 0b010, // [B] + 0b100, // [A] + 0b011, // [B,C] + 0b101, // [A,C] + 0b110 // [A,B] + ); + assertEquals(expected.size(), result.size()); + assertTrue(result.containsAll(expected)); + } + + @Test + public void testGetList_size0() { + List result = Arrays.stream(ListFabric.generateList(0)).boxed().collect(Collectors.toList()); + assertEquals(1, result.size()); + assertEquals(0, (int) result.get(0)); // Solo el conjunto vacío + } + + @Test + public void testGetList_maxSize0() { + ListFabric.MAX_SIZE = 0; + List result = Arrays.stream(ListFabric.generateList(3)).boxed().collect(Collectors.toList()); + assertEquals(1, result.size()); + assertEquals(0, (int) result.get(0)); // Solo el conjunto vacío + } + + @Test + public void testNoSubsetExceedsMaxSize() { + int size = 4; + List result = Arrays.stream(ListFabric.generateList(size)).boxed().collect(Collectors.toList()); + for (int subset : result) { + int ones = Integer.bitCount(subset); + assertTrue(ones <= ListFabric.MAX_SIZE, + "Subset " + Integer.toBinaryString(subset) + " has " + ones + " bits set"); + } + } + + @Test + public void testSymmetry_sizeEqualsMaxSize() { + int size = 3; + ListFabric.MAX_SIZE = 3; + List result = Arrays.stream(ListFabric.generateList(size)).boxed().collect(Collectors.toList()); + // All subsets should be included (2^3 = 8) + assertEquals(8, result.size()); + } +} diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/PowerSetTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/PowerSetTest.java new file mode 100644 index 0000000..34f2d01 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/PowerSetTest.java @@ -0,0 +1,89 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +public class PowerSetTest { + + private List nodeList; + + @BeforeEach + public void setUp() { + nodeList = new ArrayList<>(); + nodeList.add(new GraphNode("X")); + nodeList.add(new GraphNode("Y")); + nodeList.add(new GraphNode("Z")); + } + + @Test + public void testPowerSetWithMaxSize() { + PowerSet ps = new PowerSet(nodeList, 2); + List> result = new ArrayList<>(); + while (ps.hasMoreElements()) { + result.add(ps.nextElement()); + } + + // Verifica que no hay subconjuntos de tamaño > 2 + for (Set subset : result) { + assertTrue(subset.size() <= 2, "Subset size should be <= 2"); + } + + // Comprobamos algunos subconjuntos esperados + HashSet expected1 = new HashSet<>(); + expected1.add(nodeList.get(0)); + HashSet expected2 = new HashSet<>(); + expected2.add(nodeList.get(0)); + expected2.add(nodeList.get(1)); + assertTrue(result.contains(new HashSet<>(expected1))); + assertTrue(result.contains(new HashSet<>(expected2))); + } + + @Test + public void testPowerSetWithoutMaxSize() { + PowerSet ps = new PowerSet(nodeList); + List> result = new ArrayList<>(); + while (ps.hasMoreElements()) { + result.add(ps.nextElement()); + } + + // Número de subconjuntos debería ser 2^n + assertEquals(7, result.size()); + + // El conjunto vacío debería estar incluido + assertTrue(result.contains(new HashSet())); + + // El conjunto completo no está incluido en el resultado + assertTrue(!result.contains(new HashSet<>(nodeList))); + } + + @Test + public void testResetIndex() { + PowerSet ps = new PowerSet(nodeList); + assertTrue(ps.hasMoreElements()); + ps.nextElement(); + ps.resetIndex(); + assertTrue(ps.hasMoreElements()); + } + + @Test + public void testMaxPowerSetSizeStatic() { + new PowerSet(nodeList); // Esto actualiza el valor de maxPow + assertEquals(8L, PowerSet.maxPowerSetSize()); + } + + @Test + public void testMaxSizeIsNegativeShouldThrowException() { + assertThrows(IllegalArgumentException.class, () -> new PowerSet(nodeList, -1)); + } +} From e38ed8ff48dbcfb2377418acdd67a4b79a37fb4a Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:40:24 +0200 Subject: [PATCH 19/32] Adding Javadoc to PowerSet --- .../uclm/i3a/simd/consensusBN/PowerSet.java | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java index 6022718..bbd78c3 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java @@ -14,12 +14,33 @@ * It implements Enumeration to allow iteration over the subsets. */ public class PowerSet implements Enumeration> { - + /** + * List of nodes for which the power set is generated. + */ List nodes; + /** + * List to store the generated subsets. + */ private final List> subSets = new ArrayList<>(); + /** + * Index to track the current position in the enumeration. + */ private int index; - private int[] list; + /** + * List of integers representing the subsets in binary form. + */ + private int[] binaryList; + + /** + * A map to store the subsets with their corresponding binary representation. + * The key is the binary representation of the subset, and the value is the subset itself. + */ private HashMap> subset; + + /** + * Maximum size of the subsets to be generated. + * If set to a value less than the number of nodes, it limits the size of the subsets. + */ private static int maxPow = 0; /** @@ -34,7 +55,7 @@ public PowerSet(List nodes, int maxSize) { if (maxSize < 0) { throw new IllegalArgumentException("maxSize cannot be negative"); } - if (nodes.size() <= maxSize) { + if (maxSize >= nodes.size()) { maxSize = nodes.size(); } this.nodes = nodes; @@ -64,9 +85,9 @@ public PowerSet(List nodes) { private void initializeSubsets(int maxSize) { subset = new HashMap<>(); index = 0; - list = ListFabric.generateList(nodes.size()); + binaryList = ListFabric.generateList(nodes.size()); - for (int i : list) { + for (int i : binaryList) { Set newSubSet = new HashSet<>(); String selection = Integer.toBinaryString(i); From df40b9187af7456ad9db8531c58f9760d4d4ee21 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:04:52 +0200 Subject: [PATCH 20/32] Deprecating RandomBN --- pom.xml | 5 ++ .../uclm/i3a/simd/consensusBN/RandomBN.java | 5 ++ .../BackwardEquivalenceSearchDSepTest.java | 10 +--- .../simd/consensusBN/ConsensusBESTest.java | 23 ++++--- .../simd/consensusBN/ConsensusUnionTest.java | 43 ++++++++----- .../i3a/simd/consensusBN/GraphTestHelper.java | 60 +++++++++++++++++++ 6 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/GraphTestHelper.java diff --git a/pom.xml b/pom.xml index 2af8d6d..0159f22 100644 --- a/pom.xml +++ b/pom.xml @@ -161,6 +161,11 @@ org.jacoco jacoco-maven-plugin 0.8.10 + + + **/RandomBN.class + + diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/RandomBN.java b/src/main/java/es/uclm/i3a/simd/consensusBN/RandomBN.java index 06c89a4..5d91fd8 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/RandomBN.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/RandomBN.java @@ -13,6 +13,11 @@ import edu.cmu.tetrad.bayes.MlBayesIm.InitializationMethod; import edu.cmu.tetrad.data.*; +@Deprecated +/** + * RandomBN generates a set of random Bayesian networks (BNs) based on specified parameters. This class has been used for experiments, and is being maintained for compatibility with existing experiments. + * Further development should avoid using this class and instead use @see RandomGraph from Tetrad for generating random DAGs. + */ public class RandomBN { int seed = 0; diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java index f5d6a6a..b3ca80f 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSepTest.java @@ -18,13 +18,9 @@ class BackwardEquivalenceSearchDSepTest { private ArrayList createRandomDagList(int copies) { - RandomBN setOfDags = new RandomBN(0, 20, 50, - copies,3); - setOfDags.setMaxInDegree(4); - setOfDags.setMaxOutDegree(4); - setOfDags.generate(); - - return setOfDags.setOfRandomDags; + ArrayList setOfDags = new ArrayList<>(); + setOfDags.addAll(GraphTestHelper.generateRandomDagList(20, copies, 50, 49, 49, 49, true, 0)); + return setOfDags; } @Test diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java index 528b4d8..f704120 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusBESTest.java @@ -102,14 +102,20 @@ public void testConsensusUnionConsistency() { @Test public void testRandomBNFusion(){ - // (seed, n. variables, n egdes max, n.dags, mutation(n. de operaciones)) - RandomBN setOfDags = new RandomBN(0, 20, 50, - 4,3); - setOfDags.setMaxInDegree(4); - setOfDags.setMaxOutDegree(4); - setOfDags.generate(); - - ConsensusBES conDag = new ConsensusBES(setOfDags.setOfRandomDags); + // Creating random DAGs + ArrayList randomDagsList = new ArrayList<>(); + randomDagsList.addAll(GraphTestHelper.generateRandomDagList(20, 2, 50, 19, 19, 19, true,0)); + + // Creating ConsensusUnion instance + ConsensusUnion consensusUnionOnly = new ConsensusUnion(randomDagsList); + Dag unionDagOnly = consensusUnionOnly.union(); + + assertNotNull(unionDagOnly); + assertTrue(unionDagOnly.getNumEdges() >= 0); + assertTrue(unionDagOnly.getNodes().size() == randomDagsList.get(0).getNodes().size()); + + + ConsensusBES conDag = new ConsensusBES(randomDagsList); conDag.fusion(); Dag besDag = conDag.getFusionDag(); Dag unionDag = conDag.getUnion(); @@ -120,6 +126,7 @@ public void testRandomBNFusion(){ assertNotNull(besDag); assertNotNull(unionDag); assertNotNull(consensusUnion); + assertEquals(unionDagOnly, unionDag); assertEquals(besDag.getNodes().size(), unionDag.getNodes().size()); assert consensusNumberOfInsertedEdges >= 0; assert consensusNumberOfInsertedEdges >= totalNumberOfInsertedEdges; diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusUnionTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusUnionTest.java index e97c87f..09de508 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusUnionTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/ConsensusUnionTest.java @@ -12,6 +12,8 @@ import edu.cmu.tetrad.graph.Graph; import edu.cmu.tetrad.graph.GraphNode; import edu.cmu.tetrad.graph.Node; +import edu.cmu.tetrad.graph.RandomGraph; +import edu.cmu.tetrad.util.RandomUtil; public class ConsensusUnionTest { @@ -117,22 +119,33 @@ public void testEmptyDagListReturnsEmptyUnion() { @Test public void testRandomBNGeneratesConsensusUnionCorrectly() { - - //System.out.println("Grafos de Partida: "); - - // (seed, n. variables, n egdes aprox, n. dags, mutation) - RandomBN setOfDags = new RandomBN(0, 20, 50, - 4,3); - setOfDags.generate(); - - //for( Dag g: setOfDags.setOfRandomDags) System.out.print(g); - ConsensusUnion conDag= new ConsensusUnion(setOfDags.setOfRandomDags); - Graph g = conDag.union(); - //System.out.println("grafo consenso: "+ g); - + ArrayList randomDagsList = new ArrayList<>(); + int sizeRandomDags = 2; + int numVariables = 20; + + // Creating list of shared nodes + ArrayList sharedNodes = new ArrayList<>(); + for (int i = 0; i < numVariables; i++) { + Node node = new GraphNode("Node" + i); + sharedNodes.add(node); + } + // Setting seed + RandomUtil.getInstance().setSeed(42); + + // Generating random DAGs + for (int i = 0; i < sizeRandomDags; i++) { + Dag randomDag = RandomGraph.randomDag(sharedNodes,0,50,19,19,19,true); + randomDagsList.add(randomDag); + } + + // Applying ConsensusUnion + ConsensusUnion conDag = new ConsensusUnion(randomDagsList); + Graph g = conDag.union(); + + // Validating the resulting consensus DAG assertNotNull(g); assertTrue(g.getNumEdges() >= 0); - assertTrue(g.getNodes().size() == setOfDags.setOfRandomDags.get(0).getNodes().size()); - + assertTrue(g.getNodes().size() == randomDagsList.get(0).getNodes().size()); + } } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/GraphTestHelper.java b/src/test/java/es/uclm/i3a/simd/consensusBN/GraphTestHelper.java new file mode 100644 index 0000000..01061e8 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/GraphTestHelper.java @@ -0,0 +1,60 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; +import java.util.List; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; +import edu.cmu.tetrad.graph.RandomGraph; +import edu.cmu.tetrad.util.RandomUtil; + +public class GraphTestHelper { + + + private GraphTestHelper() { + // Private constructor to prevent instantiation + } + + /** + * Generates a list of random DAGs sharing the same set of nodes. + * + * @param numVariables Number of variables (nodes) in each DAG. + * @param numDags Number of random DAGs to generate. + * @param maxEdges Maximum number of edges in each DAG. + * @param maxInDegree Maximum in-degree for each node. + * @param maxOutDegree Maximum out-degree for each node. + * @param maxDegree Maximum degree for each node. + * @param connected Whether the generated DAGs should be connected. + * @param seed Seed for random number generation. + * @return List of randomly generated DAGs + */ + public static List generateRandomDagList(int numVariables, int numDags, int maxEdges, int maxInDegree, int maxOutDegree, int maxDegree, boolean connected, long seed) { + List randomDagsList = new ArrayList<>(); + + // Create shared nodes + List sharedNodes = new ArrayList<>(); + for (int i = 0; i < numVariables; i++) { + sharedNodes.add(new GraphNode("Node" + i)); + } + + // Set seed + RandomUtil.getInstance().setSeed(seed); + + // Generate DAGs + for (int i = 0; i < numDags; i++) { + Dag randomDag = RandomGraph.randomDag( + sharedNodes, + 0, // Latent variables (0 by default) + maxEdges, + maxDegree, + maxInDegree, + maxOutDegree, + connected + ); + randomDagsList.add(randomDag); + } + + return randomDagsList; + } +} From b2c59b997375fb4aa38fd04db4275e580bdf255e Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:16:59 +0200 Subject: [PATCH 21/32] Adding exception test cases for PairWiseConsensusBES --- .../consensusBN/PairWiseConsensusBESTest.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBESTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBESTest.java index 0c4c1cd..25a14b7 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBESTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBESTest.java @@ -1,9 +1,12 @@ package es.uclm.i3a.simd.consensusBN; +import java.util.Arrays; import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -102,4 +105,77 @@ public void testFusionIsConsistentWithInput() { assertNotNull(edges); assertFalse(edges.isEmpty(), "Fusion DAG should contain edges"); } + + // Exception throws test cases + + @Test + public void testNullDags() { + IllegalArgumentException ex1 = assertThrows(IllegalArgumentException.class, + () -> new PairWiseConsensusBES(null, createValidDag())); + assertEquals("Input DAGs cannot be null.", ex1.getMessage()); + + IllegalArgumentException ex2 = assertThrows(IllegalArgumentException.class, + () -> new PairWiseConsensusBES(createValidDag(), null)); + assertEquals("Input DAGs cannot be null.", ex2.getMessage()); + } + + @Test + public void testEmptyNodes() { + Dag dag1 = new Dag(); // no nodes + Dag dag2 = createValidDag(); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new PairWiseConsensusBES(dag1, dag2)); + assertEquals("Input DAGs must contain at least one node.", ex.getMessage()); + } + + @Test + public void testEmptyEdges() { + Node n1 = new GraphNode("X"); + Node n2 = new GraphNode("Y"); + Dag dag1 = new Dag(Arrays.asList(n1, n2)); // 2 nodes, no edges + Dag dag2 = createValidDag(); // tiene al menos un edge + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new PairWiseConsensusBES(dag1, dag2)); + assertEquals("Input DAGs must contain at least one edge.", ex.getMessage()); + } + + @Test + public void testDifferentNodeCounts() { + Dag dag1 = createValidDag(); + Dag dag2 = createValidDag(); + dag2.addNode(new GraphNode("Extra")); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new PairWiseConsensusBES(dag1, dag2)); + assertEquals("Input DAGs must have the same number of nodes.", ex.getMessage()); + } + + @Test + public void testDifferentNodeSets() { + Node n1 = new GraphNode("A"); + Node n2 = new GraphNode("B"); + Node n3 = new GraphNode("C"); + + Dag dag1 = new Dag(Arrays.asList(n1, n2)); + dag1.addDirectedEdge(n1, n2); + + Dag dag2 = new Dag(Arrays.asList(n1, n3)); // C en vez de B + dag2.addDirectedEdge(n1, n3); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> new PairWiseConsensusBES(dag1, dag2)); + assertEquals("Input DAGs must have the same set of nodes.", ex.getMessage()); + } + + // Helper para crear un DAG válido + private Dag createValidDag() { + Node a = new GraphNode("A"); + Node b = new GraphNode("B"); + + Dag dag = new Dag(Arrays.asList(a, b)); + dag.addDirectedEdge(a, b); + return dag; + } } From e1249df41522757c6c584ac2648afa31c1e8fef2 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:09:59 +0200 Subject: [PATCH 22/32] Cleaning and Testing HeuristicConsensus classes --- .../BackwardEquivalenceSearchDSep.java | 48 +++- .../i3a/simd/consensusBN/ConsensusBES.java | 6 +- .../consensusBN/HeuristicConsensusBES.java | 2 +- .../consensusBN/HeuristicConsensusBES2.java | 36 +++ .../HeuristicConsensusMVoting.java | 250 ++++++++++++++---- .../uclm/i3a/simd/consensusBN/PowerSet.java | 4 +- .../es/uclm/i3a/simd/consensusBN/Utils.java | 4 +- .../HeuristicConsensusBESTest.java | 131 +++++++++ .../HeuristicConsensusMVotingTest.java | 111 ++++++++ .../i3a/simd/consensusBN/PowerSetTest.java | 6 +- 10 files changed, 530 insertions(+), 68 deletions(-) create mode 100644 src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES2.java create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBESTest.java create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVotingTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index 9415958..7c3a52f 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -83,6 +83,16 @@ public class BackwardEquivalenceSearchDSep { */ private int numberOfRemovedEdges = 0; + /** + * Percentage threshold for edge deletion. By default, it is set to 1.0, set to another value for an heuristic search + */ + private double percentage = 1.0; + + /** + * Maximum size of the conditioning set for edge deletion. Set to Integer.MAX_VALUE by default. Another value can be set for an heuristic search. + */ + private int maxSize = Integer.MAX_VALUE; + /** * Constructor for BackwardEquivalenceSearchDSep that initializes the properties for the search with a union DAG and lists of initial and transformed DAGs. * @@ -166,7 +176,6 @@ public Dag applyBackwardEliminationWithDSeparation(){ // Rebuild the pattern to ensure the final graph is a DAG createOutputDag(); - return outputDag; } @@ -229,6 +238,11 @@ private EdgeCandidate calculateBestCandidateEdge(List edges, double score) // Getting a HashSet of hNeighbors Set hSubset=hSubsets.nextElement(); + // Checking size of hSubset + if (hSubset.size() > maxSize) { + break; // Skip to next edge if the size exceeds the maximum allowed size + } + // Checking if {naYXH} \ {hSubset} is a clique List naYXH = Utils.findNaYX(candidateTail, candidateHead, graph); naYXH.removeAll(hSubset); @@ -240,7 +254,7 @@ private EdgeCandidate calculateBestCandidateEdge(List edges, double score) double deleteEval = deleteEval(candidateTail, candidateHead, hSubset, graph); // Setting limit for deleteEval - if (!(deleteEval >= 1.0)) deleteEval = 0.0; + if (deleteEval < percentage) deleteEval = 0.0; // If the score is not better than the best score, continue double evalScore = score + deleteEval; @@ -446,15 +460,16 @@ private double scoreGraphChangeDelete(Node x, Node y, Set conditioningSet) } // Evaluating the d-separation condition across all initial DAGs + double eval = 0.0; for (Dag g : this.initialDags) { - if (!Utils.dSeparated(g, x, y, new ArrayList<>(conditioningSet))) { - localScore.put(key, 0.0); - return 0.0; + if (Utils.dSeparated(g, x, y, new ArrayList<>(conditioningSet))) { + eval++; } } + eval = eval / (double) this.initialDags.size(); - localScore.put(key, 1.0); - return 1.0; + localScore.put(key, eval); + return eval; } /** * Returns the number of edges that were inserted during the consensus union and backward equivalence search process. @@ -464,6 +479,25 @@ public int getNumberOfRemovedEdges() { return this.numberOfRemovedEdges; } + public void setPercentage(double percentage) { + if(percentage < 0.0 || percentage > 1.0) { + throw new IllegalArgumentException("Percentage must be between 0.0 and 1.0"); + } + this.percentage = percentage; + } + public void setMaxSize(int maxSize) { + if(maxSize < 0) { + throw new IllegalArgumentException("Max size must be a non-negative integer"); + } + this.maxSize = maxSize; + } + public double getPercentage() { + return this.percentage; + } + public int getMaxSize() { + return this.maxSize; + } + /** * Class representing a candidate edge for deletion in the Backward Equivalence Search. * This class encapsulates the tail and head nodes of the edge, the conditioning set used for d-separation, diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java index 83e7ab0..7403908 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java @@ -24,7 +24,7 @@ public class ConsensusBES implements Runnable { * @see ConsensusUnion * @see BackwardEquivalenceSearchDSepTest */ - private Dag outputDag; + protected Dag outputDag; /** * Instance of ConsensusUnion used to compute the consensus DAG from the input DAGs. @@ -154,6 +154,10 @@ public ArrayList getTransformedDags() { throw new IllegalStateException("Transformed DAGs have not been initialized. Please call fusion() first."); } } + + public ArrayList getInputDags() { + return this.inputDags; + } /** * Runs the ConsensusBES algorithm in a thread, performing the consensus union and the Backward Equivalence Search with D-separation. diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java index ae927d2..ca66fc7 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java @@ -118,7 +118,7 @@ public void fusion(){ Set hSubset=hSubsets.nextElement(); if(hSubset.size() > maxSize) break; double deleteEval = deleteEval(_x, _y, hSubset, graph); - if (!(deleteEval >= this.percentage)) deleteEval = 0.0; + if (deleteEval < this.percentage) deleteEval = 0.0; double evalScore = score + deleteEval; // System.out.println("Attempt removing " + _x + "-->" + _y + "(" +evalScore + ") "+ hSubset.toString()); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES2.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES2.java new file mode 100644 index 0000000..4e04c74 --- /dev/null +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES2.java @@ -0,0 +1,36 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; + +import edu.cmu.tetrad.graph.Dag; + +public class HeuristicConsensusBES2 extends ConsensusBES{ + + private final int maxSize; + private final double percentage; + + /** + * Constructor for HeuristicConsensusBES2. + * This class extends ConsensusBES to implement a heuristic approach + * for backward equivalence search in directed acyclic graphs (DAGs). + */ + public HeuristicConsensusBES2(ArrayList dags, int maxSize, double percentage) { + super(dags); + this.maxSize = maxSize; + this.percentage = percentage; + } + + // Additional methods and overrides can be added here as needed + @Override + public void fusion(){ + // 1. Apply ConsensusUnion + consensusUnion(); + // 2. Apply Heuristic BES with D-separation + BackwardEquivalenceSearchDSep bes = new BackwardEquivalenceSearchDSep(this.getUnion(), this.getInputDags(), this.getTransformedDags()); + bes.setMaxSize(maxSize); + bes.setPercentage(percentage); + this.outputDag = bes.applyBackwardEliminationWithDSeparation(); + // 3. Updating numberOfInsertedEdges + this.numberOfInsertedEdges -= bes.getNumberOfRemovedEdges(); + } +} diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVoting.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVoting.java index 1abe261..caf1f11 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVoting.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVoting.java @@ -6,48 +6,142 @@ import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Edge; import edu.cmu.tetrad.graph.EdgeListGraph; +import edu.cmu.tetrad.graph.Edges; import edu.cmu.tetrad.graph.Endpoint; import edu.cmu.tetrad.graph.Graph; import edu.cmu.tetrad.graph.Node; import static es.uclm.i3a.simd.consensusBN.Utils.pdagToDag; +/** + * The {@code HeuristicConsensusMVoting} class implements a consensus structure learning algorithm + * based on a heuristic majority voting strategy. + *

+ * Given a collection of Directed Acyclic Graphs (DAGs), this class aggregates the structures + * by counting the frequency of directed edges across all graphs and selecting those + * that meet or exceed a specified threshold. + *

+ * The heuristic helps reduce noise by filtering out weakly supported edges, and it resolves + * conflicts (such as bidirectional edges) by applying majority rules or tie-breaking strategies. + *

+ * Typical use cases include combining results from different structure learning algorithms, + * bootstrapping runs, or expert-curated networks. + *

+ * The resulting output is a new DAG that aims to reflect the most consistent edges found + * across the input graphs. + * + *

Note: Input DAGs must share the same set of nodes for the consensus to be meaningful. + * + * Example usage: + *

{@code
+ * List inputGraphs = Arrays.asList(dag1, dag2, dag3);
+ * HeuristicConsensusMVoting consensus = new HeuristicConsensusMVoting(inputGraphs, threshold);
+ * Dag consensusDag = consensus.getConsensusGraph();
+ * }
+ * + */ public class HeuristicConsensusMVoting { - ArrayList variables = null; - Dag outputDag = null; - ArrayList setOfdags = null; - double percentage = 1.0; - double [][] weight = null; + /** + * List of variables (nodes) in the consensus DAG. + * This list is derived from the nodes of the input DAGs. + */ + private ArrayList variables = null; + + /** + * The resulting output DAG after applying the heuristic consensus voting. + * This DAG contains the edges that were selected based on the majority voting strategy. + */ + private Dag outputDag = null; + + /** + * List of input DAGs used to compute the consensus. + * These DAGs are expected to have the same set of nodes. + */ + private ArrayList setOfdags = null; + + /** + * Percentage threshold for edge inclusion in the consensus DAG. + * An edge is included if it appears in at least this percentage of the input DAGs. + */ + private double percentage = 1.0; + + /** + * Weight matrix representing the frequency of edges between pairs of nodes. + * The weight[i][j] indicates how many times the edge from node i to node j appears in the input DAGs. + */ + private double [][] weight = null; + /** + * Constructor for HeuristicConsensusMVoting. + * Initializes the variables, output DAG, input DAGs, and weight matrix. + * @param setOfdags + * @param percentage + */ + public HeuristicConsensusMVoting(ArrayList setOfdags, double percentage) { + this.variables = (ArrayList) setOfdags.get(0).getNodes(); + this.outputDag = null; + this.setOfdags = setOfdags; + this.percentage = percentage; + this.weight = new double[this.variables.size()][this.variables.size()]; + setup(); + } + /** + * Sets up the HeuristicConsensusMVoting instance by validating the input DAGs + * and building the weight matrix based on the edges present in the input DAGs. + */ + private void setup() { + // Ensuring that the input DAGs are not null and have the same set of nodes + validateInputDags(); + buildWeightMatrix(); + } + + /** + * Validates the input DAGs to ensure they are not null and have the same set of nodes. + * Throws an IllegalArgumentException if any validation fails. + */ + private void validateInputDags() { + if (this.setOfdags == null || this.setOfdags.isEmpty()) + throw new IllegalArgumentException("Input DAGs cannot be null or empty."); + for(Dag dag : this.setOfdags) { + if (dag.getNodes().size() != this.variables.size()) { + throw new IllegalArgumentException("All input DAGs must have the same number of nodes."); + } + if (!dag.getNodes().containsAll(this.variables)) { + throw new IllegalArgumentException("All input DAGs must have the same set of nodes."); + } + } + } -public HeuristicConsensusMVoting(ArrayList setOfdags, double percentage) { - super(); - this.variables = (ArrayList) setOfdags.get(0).getNodes(); - this.outputDag = null; - this.setOfdags = setOfdags; - this.percentage = percentage; - this.weight = new double[this.variables.size()][this.variables.size()]; - ArrayList pdags = new ArrayList(); + /** + * Builds the weight matrix based on the edges present in the input DAGs. + * Each entry weight[i][j] is incremented for each directed edge from node i to node j + * across all input DAGs, normalized by the number of input DAGs + */ + private void buildWeightMatrix() { + ArrayList pdags = new ArrayList<>(); for(Dag g: this.setOfdags){ - Graph pd = new EdgeListGraph(new LinkedList(g.getNodes())); + Graph graph = new EdgeListGraph(new LinkedList<>(g.getNodes())); for(Edge e: g.getEdges()){ - pd.addEdge(e); + graph.addEdge(e); } - pdagToDag(pd); - pdags.add(pd); + pdagToDag(graph); + pdags.add(graph); } - + for(Graph pd: pdags){ for(Edge e: pd.getEdges()){ - Node n1 = e.getNode1(); - Node n2 = e.getNode2(); if(e.isDirected()){ - if(e.getEndpoint1() == Endpoint.ARROW){ - this.weight[variables.indexOf(n2)][variables.indexOf(n1)]+= (double) (1.0/this.setOfdags.size()); - }else{ - this.weight[variables.indexOf(n1)][variables.indexOf(n2)]+= (double) (1.0/this.setOfdags.size()); - } + Node from = Edges.getDirectedEdgeTail(e); + Node to = Edges.getDirectedEdgeHead(e); + this.weight[variables.indexOf(from)][variables.indexOf(to)] += (double) (1.0/this.setOfdags.size()); + // if(e.getEndpoint1() == Endpoint.ARROW){ + // this.weight[variables.indexOf(n2)][variables.indexOf(n1)]+= (double) (1.0/this.setOfdags.size()); + // }else{ + // this.weight[variables.indexOf(n1)][variables.indexOf(n2)]+= (double) (1.0/this.setOfdags.size()); + // } }else{ + Node n1 = e.getNode1(); + Node n2 = e.getNode2(); this.weight[variables.indexOf(n2)][variables.indexOf(n1)]+= (double) (1.0/this.setOfdags.size()); this.weight[variables.indexOf(n1)][variables.indexOf(n2)]+= (double) (1.0/this.setOfdags.size()); } @@ -55,36 +149,88 @@ public HeuristicConsensusMVoting(ArrayList setOfdags, double percentage) { } } -public Dag fusion(){ - - this.outputDag = new Dag(variables); - boolean procced = true; - int bestEdgei = 0; - int bestEdgej = 0; - double maxW = 0.0; - while(procced){ - for(int i = 0; i= maxW){ - if((this.weight[i][j] > maxW) || ((this.weight[i][j]==maxW) && (Math.random()>0.5))){ - bestEdgei = i; - bestEdgej = j; - maxW = this.weight[i][j]; + /** + * Performs the fusion of the input DAGs using a heuristic majority voting strategy. + * The method iteratively selects edges based on their weights and adds them to the output DAG + * until no more edges meet the specified percentage threshold. + * @return The resulting consensus DAG after applying the heuristic voting. + */ + public Dag fusion(){ + this.outputDag = new Dag(variables); + + while(true){ + int bestEdgei = -1; // Best edge head node index + int bestEdgej = -1; // Best edge tail node index + double maxW = 0.0; // Maximum weight found in the current iteration + + // Find the best edge based on the weight matrix + for(int i = 0; i= maxW){ + if((this.weight[i][j] > maxW) || ((this.weight[i][j]==maxW) && (Math.random()>0.5))){ + bestEdgei = i; + bestEdgej = j; + maxW = this.weight[i][j]; + } } } - } - if(maxW >= this.percentage){ - if(!this.outputDag.paths().existsDirectedPath(variables.get(bestEdgej), variables.get(bestEdgei))){ - this.outputDag.addEdge(new Edge(variables.get(bestEdgei),variables.get(bestEdgej),Endpoint.TAIL,Endpoint.ARROW)); - this.weight[bestEdgei][bestEdgej] = 0; - }else this.weight[bestEdgei][bestEdgej] = 0; - if(maxW==0) procced = false; - maxW = 0.0; - }else procced = false; + // Stop if no edge meets the threshold + if(bestEdgei == -1 || bestEdgej == -1 || maxW < percentage || maxW == 0.0) + break; + + // Add edge if it doesn't introduce a cycle + Node from = variables.get(bestEdgei); + Node to = variables.get(bestEdgej); + if(!this.outputDag.paths().existsDirectedPath(to, from)){ + this.outputDag.addEdge(new Edge(from,to,Endpoint.TAIL,Endpoint.ARROW)); + } + // Mark the edge as used by setting its weight to zero + this.weight[bestEdgei][bestEdgej] = 0; + } + + return this.outputDag; + } + + /** + * Returns the nodes (variables) of the consensus DAG. + * @return A list of nodes representing the variables in the consensus DAG. + */ + public ArrayList getVariables() { + return variables; + } + + /** + * Returns the resulting consensus DAG after applying the heuristic voting. + * @return The output DAG containing the selected edges based on the majority voting strategy. + */ + public Dag getOutputDag() { + return outputDag; + } + + /** + * Returns the list of input DAGs used to compute the consensus. + * @return A list of DAGs that were used as input for the consensus voting. + */ + public ArrayList getSetOfdags() { + return setOfdags; + } + + /** + * Returns the percentage threshold used for edge inclusion in the consensus DAG. + * @return The percentage threshold for edge inclusion. + */ + public double getPercentage() { + return percentage; + } + /** + * Returns the weight matrix representing the frequency of edges between pairs of nodes. + * Each entry weight[i][j] indicates the weight that the edge from node i to node j has in the input DAGs. + * @return The weight matrix used in the consensus voting process. + * @see #fusion() + */ + public double[][] getWeight() { + return weight; } - - return this.outputDag; -} } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java index bbd78c3..cfdd82e 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java @@ -41,7 +41,7 @@ public class PowerSet implements Enumeration> { * Maximum size of the subsets to be generated. * If set to a value less than the number of nodes, it limits the size of the subsets. */ - private static int maxPow = 0; + private int maxPow = 0; /** * Builds a PowerSet with subsets of the given nodes, limited to a maximum size. @@ -136,7 +136,7 @@ public void resetIndex(){ * This method calculates the size of the power set as 2 raised to the power of the maximum number of nodes. * @return The maximum size of the power set. */ - public static long maxPowerSetSize() { + public long maxPowerSetSize() { return (long) Math.pow(2,maxPow); } } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java index 751303e..b8f7298 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java @@ -53,7 +53,7 @@ public static void pdagToDag(Graph graph){ for(Node node : nodes){ x = node; //Checking if the node has no outgoing edges - if(graphAux.getChildren(node).size() != 0) + if(!graphAux.getChildren(node).isEmpty()) continue; //Checking if the neighbors form a clique if(!GraphUtils.isClique(graphAux.getAdjacentNodes(x), graphAux)) @@ -70,7 +70,7 @@ public static void pdagToDag(Graph graph){ break; // We break the loop to start again with the new graphAux without the removed node. } nodes.remove(x); - }while(nodes.size() > 0); + }while(!nodes.isEmpty()); } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBESTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBESTest.java new file mode 100644 index 0000000..36181b2 --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBESTest.java @@ -0,0 +1,131 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.Node; + +public class HeuristicConsensusBESTest { + private ArrayList inputDags; + private ArrayList alpha; + + @BeforeEach + public void setUp() { + inputDags = new ArrayList<>(); + + // We use 4 nodes for the DAGs + Node nodeA = new GraphNode("A"); + Node nodeB = new GraphNode("B"); + Node nodeC = new GraphNode("C"); + Node nodeD = new GraphNode("D"); + + // Create first DAG with these edges: A -> B, A -> C, B -> D, C -> D + Dag dag1 = new Dag(); + dag1.addNode(nodeA); + dag1.addNode(nodeB); + dag1.addNode(nodeC); + dag1.addNode(nodeD); + + // Adding directed edges to the DAG + dag1.addDirectedEdge(nodeA, nodeB); + dag1.addDirectedEdge(nodeA, nodeC); + dag1.addDirectedEdge(nodeB, nodeD); + dag1.addDirectedEdge(nodeC, nodeD); + + // Adding the DAG to the list + inputDags.add(dag1); + + // Create second DAG with these edges: D -> C, D -> B, C -> A, B -> A + Dag dag2 = new Dag(); + dag2.addNode(nodeA); + dag2.addNode(nodeB); + dag2.addNode(nodeC); + dag2.addNode(nodeD); + + // Adding directed edges to the second DAG + dag2.addDirectedEdge(nodeD, nodeC); + dag2.addDirectedEdge(nodeD, nodeB); + dag2.addDirectedEdge(nodeC, nodeA); + dag2.addDirectedEdge(nodeB, nodeA); + + // Adding the second DAG to the list + inputDags.add(dag2); + + // Apply AlphaOrder algorithm to these dags: + AlphaOrder alphaOrder = new AlphaOrder(inputDags); + alphaOrder.computeAlpha(); + alpha = alphaOrder.getOrder(); + } + + @Test + public void testConsistencySimple(){ + // Create a HeuristicConsensusBES instance with the input DAGs + HeuristicConsensusBES consensusBES = new HeuristicConsensusBES(inputDags,0.5); + // Perform the fusion to create the consensus DAG + consensusBES.fusion(); + Dag consensusBESDag = consensusBES.getFusion(); + + // Doing the same with HeuristicConsensusBES2 + HeuristicConsensusBES2 consensusBES2 = new HeuristicConsensusBES2(inputDags, 10,0.5); + consensusBES2.fusion(); + Dag consensusBESDag2 = consensusBES2.getFusionDag(); + // Check if the consensus DAG is not null + System.out.println("consensusBESDag: " + consensusBESDag); + System.out.println("consensusBESDag2: " + consensusBESDag2); + assertNotNull(consensusBESDag, "The consensus DAG1 should not be null"); + assertNotNull(consensusBESDag2, "The consensus DAG2 should not be null"); + + // Check if the consensus DAG has the expected number of nodes and edges + assertEquals(4, consensusBESDag.getNumNodes(), "The consensus DAG1 should have 4 nodes"); + assertEquals(4, consensusBESDag2.getNumNodes(), "The consensus DAG2 should have 4 nodes"); + assertTrue(consensusBESDag.getNumEdges() > 0); + assertTrue(consensusBESDag2.getNumEdges() > 0); + + // Check that both dags are equal + assertEquals(consensusBESDag, consensusBESDag2, "The consensus DAGs should be equal"); + assertEquals(consensusBESDag.getNodes().size(), consensusBESDag2.getNodes().size(), "The number of nodes in the consensus DAGs should be equal"); + assertEquals(consensusBESDag.getEdges().size(), consensusBESDag2.getEdges().size(), "The number of edges in the consensus DAGs should be equal"); + + + // Additional checks can be added here based on specific properties of the consensus DAG + } + + @Test + public void testConsistencyComplex(){ + ArrayList complexInputDags = new ArrayList<>(); + complexInputDags.addAll(GraphTestHelper.generateRandomDagList(20, 4, 80, 99, 99, 99, true, 42)); + + // Create a HeuristicConsensusBES instance with the complex input DAGs + HeuristicConsensusBES consensusBES = new HeuristicConsensusBES(complexInputDags, 0.5); + // Perform the fusion to create the consensus DAG + consensusBES.fusion(); + Dag consensusBESDag = consensusBES.getFusion(); + // Doing the same with HeuristicConsensusBES2 + HeuristicConsensusBES2 consensusBES2 = new HeuristicConsensusBES2(complexInputDags, 10, 0.5); + consensusBES2.fusion(); + Dag consensusBESDag2 = consensusBES2.getFusionDag(); + // Check if the consensus DAG is not null + System.out.println("consensusBESDag: " + consensusBESDag); + System.out.println("consensusBESDag2: " + consensusBESDag2); + assertNotNull(consensusBESDag, "The consensus DAG1 should not be null"); + assertNotNull(consensusBESDag2, "The consensus DAG2 should not be null"); + // Check if the consensus DAG has the expected number of nodes and edges + assertEquals(20, consensusBESDag.getNumNodes(), "The consensus DAG1 should have 20 nodes"); + assertEquals(20, consensusBESDag2.getNumNodes(), "The consensus DAG2 should have 20 nodes"); + assertTrue(consensusBESDag.getNumEdges() > 0); + assertTrue(consensusBESDag2.getNumEdges() > 0); + // Check that both dags are equal + assertEquals(consensusBESDag, consensusBESDag2, "The consensus DAGs should be equal"); + assertEquals(consensusBESDag.getNodes().size(), consensusBESDag2.getNodes().size(), "The number of nodes in the consensus DAGs should be equal"); + assertEquals(consensusBESDag.getEdges().size(), consensusBESDag2.getEdges().size(), "The number of edges in the consensus DAGs should be equal"); + + } + +} diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVotingTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVotingTest.java new file mode 100644 index 0000000..253c19e --- /dev/null +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVotingTest.java @@ -0,0 +1,111 @@ +package es.uclm.i3a.simd.consensusBN; + +import java.util.ArrayList; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import edu.cmu.tetrad.graph.Dag; +import edu.cmu.tetrad.graph.GraphNode; +import edu.cmu.tetrad.graph.GraphUtils; +import edu.cmu.tetrad.graph.Node; + +public class HeuristicConsensusMVotingTest { + + private Dag createSimpleDag(String from, String to) { + Node n1 = new GraphNode(from); + Node n2 = new GraphNode(to); + Dag dag = new Dag(Arrays.asList(n1, n2)); + dag.addDirectedEdge(n1, n2); + return dag; + } + + @Test + public void testFusionCreatesDAGWithExpectedEdges() { + Dag dag1 = createSimpleDag("A", "B"); + Dag dag2 = createSimpleDag("A", "B"); + ArrayList dags = new ArrayList<>(Arrays.asList(dag1, dag2)); + + HeuristicConsensusMVoting mvoting = new HeuristicConsensusMVoting(dags, 0.5); + Dag consensus = mvoting.fusion(); + + assertNotNull(consensus); + assertTrue(GraphUtils.isDag(consensus)); + assertEquals(2, consensus.getNumNodes()); + assertEquals(1, consensus.getNumEdges()); + assertTrue(consensus.isParentOf(getNodeByName(consensus, "A"), getNodeByName(consensus, "B"))); + + // Test getters + assertEquals(0.5, mvoting.getPercentage()); + assertEquals(2, mvoting.getVariables().size()); + assertEquals(2, mvoting.getWeight().length); + assertEquals(2, mvoting.getWeight()[0].length); + assertEquals(2, mvoting.getWeight()[1].length); + assertEquals(0.0, mvoting.getWeight()[0][1]); + assertEquals(0.0, mvoting.getWeight()[1][0]); + assertEquals(dags, mvoting.getSetOfdags()); + assertEquals(consensus, mvoting.getOutputDag()); + + } + + @Test + public void testFusionDoesNotAddLowWeightEdges() { + Dag dag1 = createSimpleDag("A", "B"); + Dag dag2 = createSimpleDag("B", "A"); // Conflicting direction + + ArrayList dags = new ArrayList<>(Arrays.asList(dag1, dag2)); + HeuristicConsensusMVoting mvoting = new HeuristicConsensusMVoting(dags, 0.75); + Dag consensus = mvoting.fusion(); + + // Expect no edge because weight is 0.5 < 0.75 + assertEquals(0, consensus.getNumEdges()); + } + + @Test + public void testFusionDoesNotCreateCycle() { + Node a = new GraphNode("A"); + Node b = new GraphNode("B"); + Node c = new GraphNode("C"); + + Dag dag1 = new Dag(Arrays.asList(a, b, c)); + dag1.addDirectedEdge(a, b); + dag1.addDirectedEdge(b, c); + + Dag dag2 = new Dag(Arrays.asList(a, b, c)); + dag2.addDirectedEdge(a, b); + dag2.addDirectedEdge(b, c); + + ArrayList dags = new ArrayList<>(Arrays.asList(dag1, dag2)); + HeuristicConsensusMVoting mvoting = new HeuristicConsensusMVoting(dags, 0.5); + Dag consensus = mvoting.fusion(); + + assertTrue(GraphUtils.isDag(consensus), "La fusión no debe crear ciclos."); + } + + @Test + public void testWeightMatrixCorrectlyComputed() { + Dag dag1 = createSimpleDag("A", "B"); + Dag dag2 = createSimpleDag("A", "B"); + ArrayList dags = new ArrayList<>(Arrays.asList(dag1, dag2)); + + HeuristicConsensusMVoting mvoting = new HeuristicConsensusMVoting(dags, 0.5); + + int indexA = mvoting.getVariables().indexOf(new GraphNode("A")); + int indexB = mvoting.getVariables().indexOf(new GraphNode("B")); + + double weightAB = mvoting.getWeight()[indexA][indexB]; + double expectedWeight = 1.0; // Dos DAGS, misma dirección + + assertEquals(expectedWeight, weightAB, 1e-6); + } + + private Node getNodeByName(Dag dag, String name) { + return dag.getNodes().stream() + .filter(n -> n.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Node not found: " + name)); + } +} diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/PowerSetTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/PowerSetTest.java index 34f2d01..437d97c 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/PowerSetTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/PowerSetTest.java @@ -77,9 +77,9 @@ public void testResetIndex() { } @Test - public void testMaxPowerSetSizeStatic() { - new PowerSet(nodeList); // Esto actualiza el valor de maxPow - assertEquals(8L, PowerSet.maxPowerSetSize()); + public void testMaxPowerSetSize() { + PowerSet powerSet = new PowerSet(nodeList); // Esto actualiza el valor de maxPow + assertEquals(8L, powerSet.maxPowerSetSize()); } @Test From 9120f8d5591b09dabb1b8b757d4abb758906011c Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:27:17 +0200 Subject: [PATCH 23/32] Cleanup ConsensusBES --- .../es/uclm/i3a/simd/consensusBN/ConsensusBES.java | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java index 7403908..8d42e72 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java @@ -1,9 +1,7 @@ package es.uclm.i3a.simd.consensusBN; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import edu.cmu.tetrad.graph.Dag; import edu.cmu.tetrad.graph.Node; @@ -59,12 +57,7 @@ public class ConsensusBES implements Runnable { */ int numberOfInsertedEdges = 0; - /** - * Local score map used to store the scores of graph changes during the Backward Equivalence Search. - * The key is a string representation of the nodes and their conditioning set, and the value is the score associated with that configuration. - */ - private final Map localScore = new HashMap<>(); - + /** * Constructor for ConsensusBES that initializes the union process with a list of DAGs. * It creates an instance of ConsensusUnion to compute the consensus DAG. @@ -155,6 +148,11 @@ public ArrayList getTransformedDags() { } } + /** + * Returns the list of input DAGs used in this ConsensusBES. + * This method retrieves the original DAGs that were provided to the ConsensusBES constructor. + * @return the list of input DAGs. + */ public ArrayList getInputDags() { return this.inputDags; } From 45f5cb3f158ac1dfe6dd273c9f05a73706e6c44b Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:30:02 +0200 Subject: [PATCH 24/32] Updating HeuristicConsensusBES with clean version --- .../consensusBN/HeuristicConsensusBES.java | 430 +----------------- .../consensusBN/HeuristicConsensusBES2.java | 36 -- .../HeuristicConsensusBESTest.java | 131 ------ 3 files changed, 24 insertions(+), 573 deletions(-) delete mode 100644 src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES2.java delete mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBESTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java index ca66fc7..93a8e41 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java @@ -1,418 +1,36 @@ package es.uclm.i3a.simd.consensusBN; import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; import edu.cmu.tetrad.graph.Dag; -import edu.cmu.tetrad.graph.Edge; -import edu.cmu.tetrad.graph.EdgeListGraph; -import edu.cmu.tetrad.graph.Edges; -import edu.cmu.tetrad.graph.Endpoint; -import edu.cmu.tetrad.graph.Graph; -import edu.cmu.tetrad.graph.Node; -import edu.cmu.tetrad.search.utils.MeekRules; -import edu.cmu.tetrad.search.utils.GraphSearchUtils; -import static es.uclm.i3a.simd.consensusBN.Utils.pdagToDag; +public class HeuristicConsensusBES extends ConsensusBES{ + private final int maxSize; + private final double percentage; - -public class HeuristicConsensusBES { - - ArrayList alpha = null; - Dag outputDag = null; - AlphaOrder heuristic = null; - TransformDags imaps2alpha = null; - ArrayList setOfdags = null; - ArrayList setOfOutDags = null; - Dag union = null; - int numberOfInsertedEdges = 0; - double percentage = 1.0; - int maxSize = 10; - - Map localScore = new HashMap(); - - public HeuristicConsensusBES(ArrayList dags, double percentage){ - this.setOfdags = dags; - this.heuristic = new AlphaOrder(this.setOfdags); - this.heuristic.computeAlpha(); - this.alpha = this.heuristic.getOrder(); - this.imaps2alpha = new TransformDags(this.setOfdags,this.alpha); - this.imaps2alpha.transform(); - this.numberOfInsertedEdges = imaps2alpha.getNumberOfInsertedEdges(); - this.setOfOutDags = imaps2alpha.getSetOfOutputDags(); - this.percentage = percentage; - } - - - public int getNumberOfInsertedEdges(){ - return this.numberOfInsertedEdges; - } - - private void consensusUnion(){ - - this.union = new Dag(this.alpha); - for(Node nodei: this.alpha){ - for(Dag d : this.imaps2alpha.getSetOfOutputDags()){ - Listparent = d.getParents(nodei); - for(Node pa: parent){ - if(!this.union.isParentOf(pa, nodei)) this.union.addEdge(new Edge(pa,nodei,Endpoint.TAIL,Endpoint.ARROW)); - } - } - - } - - } - - // private methods for searching - - - - public void fusion(){ - - // System.out.println("\n** BACKWARD ELIMINATION SEARCH (BES)"); - //PowerSetFabric.setMode(PowerSetFabric.MODE_BES); - double score = 0; - double bestScore = score; - Graph graph = null; - - consensusUnion(); - graph = new EdgeListGraph(new LinkedList(this.union.getNodes())); - for(Edge e: this.union.getEdges()){ - graph.addEdge(e); - } - - //SearchGraphUtils.dagToPdag(graph); - rebuildPattern(graph); - Node x, y; - Set t = new HashSet(); - do { - x = y = null; - Set edges1 = graph.getEdges(); - List edges = new ArrayList(); - - for (Edge edge : edges1) { - Node _x = edge.getNode1(); - Node _y = edge.getNode2(); - - if (Edges.isUndirectedEdge(edge)) { - edges.add(Edges.directedEdge(_x, _y)); - edges.add(Edges.directedEdge(_y, _x)); - } else { - edges.add(edge); - } - } - for (Edge edge : edges) { - Node _x = Edges.getDirectedEdgeTail(edge); - Node _y = Edges.getDirectedEdgeHead(edge); - - List hNeighbors = getHNeighbors(_x, _y, graph); -// List> hSubsets = powerSet(hNeighbors); - PowerSet hSubsets= new PowerSet(hNeighbors);//PowerSetFabric.getPowerSet(_x,_y,hNeighbors); - while(hSubsets.hasMoreElements()) { - Set hSubset=hSubsets.nextElement(); - if(hSubset.size() > maxSize) break; - double deleteEval = deleteEval(_x, _y, hSubset, graph); - if (deleteEval < this.percentage) deleteEval = 0.0; - double evalScore = score + deleteEval; - - // System.out.println("Attempt removing " + _x + "-->" + _y + "(" +evalScore + ") "+ hSubset.toString()); - - if (!(evalScore > bestScore)) { - continue; - } - - // INICIO TEST 1 - List naYXH = Utils.findNaYX(_x, _y, graph); - naYXH.removeAll(hSubset); - if (!isClique(naYXH, graph)) { -// hSubsets.firstTest(true); // Si pasa para H entonces pasa para cualquier H' | H' contiene H - continue; - } - // FIN TEST 1 - - bestScore = evalScore; - x = _x; - y = _y; - t = hSubset; - break; - } - - } - if (x != null) { - - //System.out.println("DELETE " + graph.getEdge(x, y) + t.toString() + " (" +bestScore + ")"); - - delete(x, y, t, graph); - rebuildPattern(graph); - this.numberOfInsertedEdges--; -// if(graph.existsDirectedCycle()){ - -// System.out.println("Hay un ciclo: "+x.toString()+" "+y.toString()); -// System.out.println("Grafo: "+graph.toString()); -// System.exit(0); -// } - score = bestScore; - } - } while (x != null); - -// System.out.println("Pdag: "+ graph.toString()); - pdagToDag(graph); -// System.out.println("PdagToDag"+graph.toString()); - this.outputDag = new Dag(); - for (Node node : graph.getNodes()) this.outputDag.addNode(node); - Node nodeT, nodeH; - for (Edge e : graph.getEdges()){ - if(!e.isDirected()) continue; - Endpoint endpoint1 = e.getEndpoint1(); - if (endpoint1.equals(Endpoint.ARROW)){ - nodeT = e.getNode1(); - nodeH = e.getNode2(); - }else{ - nodeT = e.getNode2(); - nodeH = e.getNode1(); - } - if(!this.outputDag.paths().existsDirectedPath(nodeT, nodeH)) this.outputDag.addEdge(e); - } -// System.out.println("DAG: "+this.outputDag.toString()); - } - - - - private static void delete(Node x, Node y, Set subset, Graph graph) { - graph.removeEdges(x, y); - - for (Node aSubset : subset) { - if (!graph.isParentOf(aSubset, x) && !graph.isParentOf(x, aSubset)) { - graph.removeEdge(x, aSubset); - graph.addDirectedEdge(x, aSubset); - } - graph.removeEdge(y, aSubset); - graph.addDirectedEdge(y, aSubset); - } + /** + * Constructor for HeuristicConsensusBES2. + * This class extends ConsensusBES to implement a heuristic approach + * for backward equivalence search in directed acyclic graphs (DAGs). + */ + public HeuristicConsensusBES(ArrayList dags, int maxSize, double percentage) { + super(dags); + this.maxSize = maxSize; + this.percentage = percentage; } - - private void rebuildPattern(Graph graph) { - GraphSearchUtils.basicCpdag(graph); - pdag(graph); - } - - /** - * Fully direct a graph with background knowledge. I am not sure how to - * adapt Chickering's suggested algorithm above (dagToPdag) to incorporate - * background knowledge, so I am also implementing this algorithm based on - * Meek's 1995 UAI paper. Notice it is the same implemented in PcSearch. - *

*IMPORTANT!* *It assumes all colliders are oriented, as well as - * arrows dictated by time order.* - * - * ELIMINADO BACKGROUND KNOWLEDGE - */ - private void pdag(Graph graph) { - MeekRules rules = new MeekRules(); - rules.setMeekPreventCycles(true); - rules.orientImplied(graph); - } - - - private static boolean isClique(List set, Graph graph) { - List setv = new LinkedList(set); - for (int i = 0; i < setv.size() - 1; i++) { - for (int j = i + 1; j < setv.size(); j++) { - if (!graph.isAdjacentTo(setv.get(i), setv.get(j))) { - return false; - } - } - } - return true; - } - - private static List getHNeighbors(Node x, Node y, Graph graph) { - List hNeighbors = new LinkedList(graph.getAdjacentNodes(y)); - hNeighbors.retainAll(graph.getAdjacentNodes(x)); - - for (int i = hNeighbors.size() - 1; i >= 0; i--) { - Node z = hNeighbors.get(i); - Edge edge = graph.getEdge(y, z); - if (!Edges.isUndirectedEdge(edge)) { - hNeighbors.remove(z); - } - } - - return hNeighbors; - } - - - double deleteEval(Node x, Node y, Set h, Graph graph){ - - Set set1 = new HashSet(Utils.findNaYX(x, y, graph)); - set1.removeAll(h); - set1.addAll(graph.getParents(y)); - set1.remove(x); - return scoreGraphChangeDelete(y, x, set1); // calcular si y esta d-separado de x dado el set1 en cada grafo. - - } - - double scoreGraphChangeDelete(Node y, Node x, Set set){ - - String key = y.getName()+x.getName()+set.toString(); - Double val = this.localScore.get(key); - if(val == null){ - double eval = 0.0; - LinkedList conditioning = new LinkedList(); - conditioning.addAll(set); - for(Dag g: this.setOfdags){ - if(dSeparated(g, y, x, conditioning)) ++eval; - } - eval = eval / (double) this.setOfdags.size(); - val = eval; - this.localScore.put(key, val); - return eval; - }else{ - return val.doubleValue(); - } - } - - - - boolean dSeparated(Dag g, Node x, Node y, LinkedList cond){ - - LinkedList open = new LinkedList(); - HashMap close = new HashMap(); - open.add(x); - open.add(y); - open.addAll(cond); - while (open.size() != 0){ - Node a = open.getFirst(); - open.remove(a); - close.put(a.toString(),a); - List pa =g.getParents(a); - for(Node p : pa){ - if(close.get(p.toString()) == null){ - if(!open.contains(p)) open.addLast(p); - } - } - } - - Graph aux = new EdgeListGraph(); - - for (Node node : g.getNodes()) aux.addNode(node); - Node nodeT, nodeH; - for (Edge e : g.getEdges()){ - if(!e.isDirected()) continue; - nodeT = e.getNode1(); - nodeH = e.getNode2(); - if((close.get(nodeH.toString())!=null)&&(close.get(nodeT.toString())!=null)){ - Edge newEdge = new Edge(e.getNode1(),e.getNode2(),e.getEndpoint1(),e.getEndpoint2()); - aux.addEdge(newEdge); - } - } - - close = new HashMap(); - for(Edge e: aux.getEdges()){ - if(e.isDirected()){ - Node h; - if(e.getEndpoint1()==Endpoint.ARROW){ - h = e.getNode1(); - }else h = e.getNode2(); - if(close.get(h.toString())==null){ - close.put(h.toString(),h); - List pa = aux.getParents(h); - if(pa.size()>1){ - for(int i = 0 ; i< pa.size() - 1; i++) - for(int j = i+1; j < pa.size(); j++){ - Node p1 = pa.get(i); - Node p2 = pa.get(j); - boolean found = false; - for(Edge edge : aux.getEdges()){ - if(edge.getNode1().equals(p1)&&(edge.getNode2().equals(p2))){ - found = true; - break; - } - if(edge.getNode2().equals(p1)&&(edge.getNode1().equals(p2))){ - found = true; - break; - } - } - if(!found) aux.addUndirectedEdge(p1, p2); - } - } - - } - } - } - - for(Edge e: aux.getEdges()){ - if(e.isDirected()){ - e.setEndpoint1(Endpoint.TAIL); - e.setEndpoint2(Endpoint.TAIL); - } - } - - aux.removeNodes(cond); - - open = new LinkedList(); - close = new HashMap(); - open.add(x); - while (open.size() != 0){ - Node a = open.getFirst(); - if(a.equals(y)) return false; - open.remove(a); - close.put(a.toString(),a); - List pa =aux.getAdjacentNodes(a); - for(Node p : pa){ - if(close.get(p.toString()) == null){ - if(!open.contains(p)) open.addLast(p); - } - } - } - - return true; - } - - public Dag getFusion(){ - - return this.outputDag; - } - - - public static void main(String args[]) { - - - System.out.println("Grafos de Partida: "); - - // (seed, n. variables, n egdes aprox, n.dags, mutation) - RandomBN setOfBNs = new RandomBN(0, Integer.parseInt(args[0]), Integer.parseInt(args[1]), - Integer.parseInt(args[2]),Integer.parseInt(args[3])); - setOfBNs.setMaxInDegree(3); - setOfBNs.setMaxOutDegree(3); - setOfBNs.generate(); - - for(int i = 0; i< setOfBNs.setOfRandomBNs.size(); i++){ - System.out.println("red de partida: "+i); - System.out.println("---------------------"); - System.out.println("Grafo: "); - System.out.println(setOfBNs.setOfRandomDags.get(i).getDegree()+" "+ setOfBNs.setOfRandomDags.get(i).getNumEdges()); -// System.out.println("Probabilidades: "); -// System.out.println(setOfBNs.setOfRandomBNs.get(i).toString()); -// System.out.println("_____________________"); -// System.out.println("Datos Simulados"); -// System.out.println(setOfBNs.setOfSampledBNs.get(i).toString()); - - - } - // - HeuristicConsensusBES conDag= null; - - conDag = new HeuristicConsensusBES(setOfBNs.setOfRandomDags,1.0); - conDag.fusion(); - Dag g = conDag.getFusion(); - System.out.println("grafo de partida Union: "+conDag.union.getDegree()+" "+ conDag.union.getNumEdges()); - System.out.println("grafo consenso: "+ g.getDegree() +" Complejidad de la Fusion: "+ conDag.getNumberOfInsertedEdges()+ " "+ conDag.outputDag.getNumEdges()); + // Additional methods and overrides can be added here as needed + @Override + public void fusion(){ + // 1. Apply ConsensusUnion + consensusUnion(); + // 2. Apply Heuristic BES with D-separation + BackwardEquivalenceSearchDSep bes = new BackwardEquivalenceSearchDSep(this.getUnion(), this.getInputDags(), this.getTransformedDags()); + bes.setMaxSize(maxSize); + bes.setPercentage(percentage); + this.outputDag = bes.applyBackwardEliminationWithDSeparation(); + // 3. Updating numberOfInsertedEdges + this.numberOfInsertedEdges -= bes.getNumberOfRemovedEdges(); } } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES2.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES2.java deleted file mode 100644 index 4e04c74..0000000 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES2.java +++ /dev/null @@ -1,36 +0,0 @@ -package es.uclm.i3a.simd.consensusBN; - -import java.util.ArrayList; - -import edu.cmu.tetrad.graph.Dag; - -public class HeuristicConsensusBES2 extends ConsensusBES{ - - private final int maxSize; - private final double percentage; - - /** - * Constructor for HeuristicConsensusBES2. - * This class extends ConsensusBES to implement a heuristic approach - * for backward equivalence search in directed acyclic graphs (DAGs). - */ - public HeuristicConsensusBES2(ArrayList dags, int maxSize, double percentage) { - super(dags); - this.maxSize = maxSize; - this.percentage = percentage; - } - - // Additional methods and overrides can be added here as needed - @Override - public void fusion(){ - // 1. Apply ConsensusUnion - consensusUnion(); - // 2. Apply Heuristic BES with D-separation - BackwardEquivalenceSearchDSep bes = new BackwardEquivalenceSearchDSep(this.getUnion(), this.getInputDags(), this.getTransformedDags()); - bes.setMaxSize(maxSize); - bes.setPercentage(percentage); - this.outputDag = bes.applyBackwardEliminationWithDSeparation(); - // 3. Updating numberOfInsertedEdges - this.numberOfInsertedEdges -= bes.getNumberOfRemovedEdges(); - } -} diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBESTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBESTest.java deleted file mode 100644 index 36181b2..0000000 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBESTest.java +++ /dev/null @@ -1,131 +0,0 @@ -package es.uclm.i3a.simd.consensusBN; - -import java.util.ArrayList; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import edu.cmu.tetrad.graph.Dag; -import edu.cmu.tetrad.graph.GraphNode; -import edu.cmu.tetrad.graph.Node; - -public class HeuristicConsensusBESTest { - private ArrayList inputDags; - private ArrayList alpha; - - @BeforeEach - public void setUp() { - inputDags = new ArrayList<>(); - - // We use 4 nodes for the DAGs - Node nodeA = new GraphNode("A"); - Node nodeB = new GraphNode("B"); - Node nodeC = new GraphNode("C"); - Node nodeD = new GraphNode("D"); - - // Create first DAG with these edges: A -> B, A -> C, B -> D, C -> D - Dag dag1 = new Dag(); - dag1.addNode(nodeA); - dag1.addNode(nodeB); - dag1.addNode(nodeC); - dag1.addNode(nodeD); - - // Adding directed edges to the DAG - dag1.addDirectedEdge(nodeA, nodeB); - dag1.addDirectedEdge(nodeA, nodeC); - dag1.addDirectedEdge(nodeB, nodeD); - dag1.addDirectedEdge(nodeC, nodeD); - - // Adding the DAG to the list - inputDags.add(dag1); - - // Create second DAG with these edges: D -> C, D -> B, C -> A, B -> A - Dag dag2 = new Dag(); - dag2.addNode(nodeA); - dag2.addNode(nodeB); - dag2.addNode(nodeC); - dag2.addNode(nodeD); - - // Adding directed edges to the second DAG - dag2.addDirectedEdge(nodeD, nodeC); - dag2.addDirectedEdge(nodeD, nodeB); - dag2.addDirectedEdge(nodeC, nodeA); - dag2.addDirectedEdge(nodeB, nodeA); - - // Adding the second DAG to the list - inputDags.add(dag2); - - // Apply AlphaOrder algorithm to these dags: - AlphaOrder alphaOrder = new AlphaOrder(inputDags); - alphaOrder.computeAlpha(); - alpha = alphaOrder.getOrder(); - } - - @Test - public void testConsistencySimple(){ - // Create a HeuristicConsensusBES instance with the input DAGs - HeuristicConsensusBES consensusBES = new HeuristicConsensusBES(inputDags,0.5); - // Perform the fusion to create the consensus DAG - consensusBES.fusion(); - Dag consensusBESDag = consensusBES.getFusion(); - - // Doing the same with HeuristicConsensusBES2 - HeuristicConsensusBES2 consensusBES2 = new HeuristicConsensusBES2(inputDags, 10,0.5); - consensusBES2.fusion(); - Dag consensusBESDag2 = consensusBES2.getFusionDag(); - // Check if the consensus DAG is not null - System.out.println("consensusBESDag: " + consensusBESDag); - System.out.println("consensusBESDag2: " + consensusBESDag2); - assertNotNull(consensusBESDag, "The consensus DAG1 should not be null"); - assertNotNull(consensusBESDag2, "The consensus DAG2 should not be null"); - - // Check if the consensus DAG has the expected number of nodes and edges - assertEquals(4, consensusBESDag.getNumNodes(), "The consensus DAG1 should have 4 nodes"); - assertEquals(4, consensusBESDag2.getNumNodes(), "The consensus DAG2 should have 4 nodes"); - assertTrue(consensusBESDag.getNumEdges() > 0); - assertTrue(consensusBESDag2.getNumEdges() > 0); - - // Check that both dags are equal - assertEquals(consensusBESDag, consensusBESDag2, "The consensus DAGs should be equal"); - assertEquals(consensusBESDag.getNodes().size(), consensusBESDag2.getNodes().size(), "The number of nodes in the consensus DAGs should be equal"); - assertEquals(consensusBESDag.getEdges().size(), consensusBESDag2.getEdges().size(), "The number of edges in the consensus DAGs should be equal"); - - - // Additional checks can be added here based on specific properties of the consensus DAG - } - - @Test - public void testConsistencyComplex(){ - ArrayList complexInputDags = new ArrayList<>(); - complexInputDags.addAll(GraphTestHelper.generateRandomDagList(20, 4, 80, 99, 99, 99, true, 42)); - - // Create a HeuristicConsensusBES instance with the complex input DAGs - HeuristicConsensusBES consensusBES = new HeuristicConsensusBES(complexInputDags, 0.5); - // Perform the fusion to create the consensus DAG - consensusBES.fusion(); - Dag consensusBESDag = consensusBES.getFusion(); - // Doing the same with HeuristicConsensusBES2 - HeuristicConsensusBES2 consensusBES2 = new HeuristicConsensusBES2(complexInputDags, 10, 0.5); - consensusBES2.fusion(); - Dag consensusBESDag2 = consensusBES2.getFusionDag(); - // Check if the consensus DAG is not null - System.out.println("consensusBESDag: " + consensusBESDag); - System.out.println("consensusBESDag2: " + consensusBESDag2); - assertNotNull(consensusBESDag, "The consensus DAG1 should not be null"); - assertNotNull(consensusBESDag2, "The consensus DAG2 should not be null"); - // Check if the consensus DAG has the expected number of nodes and edges - assertEquals(20, consensusBESDag.getNumNodes(), "The consensus DAG1 should have 20 nodes"); - assertEquals(20, consensusBESDag2.getNumNodes(), "The consensus DAG2 should have 20 nodes"); - assertTrue(consensusBESDag.getNumEdges() > 0); - assertTrue(consensusBESDag2.getNumEdges() > 0); - // Check that both dags are equal - assertEquals(consensusBESDag, consensusBESDag2, "The consensus DAGs should be equal"); - assertEquals(consensusBESDag.getNodes().size(), consensusBESDag2.getNodes().size(), "The number of nodes in the consensus DAGs should be equal"); - assertEquals(consensusBESDag.getEdges().size(), consensusBESDag2.getEdges().size(), "The number of edges in the consensus DAGs should be equal"); - - } - -} From d72eabfaf28cd238033b7237acc2e146c42a8563 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:30:23 +0200 Subject: [PATCH 25/32] Adding CI github action workflow and badge --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ README.md | 2 ++ 2 files changed, 34 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..173acca --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI - Maven Tests + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' # puedes cambiarlo por zulu, adopt, etc. + java-version: '17' # cambia a 11 o el que uses en tu proyecto + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Build and Test with Maven + run: mvn clean test diff --git a/README.md b/README.md index 9ee7a1b..b88878d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # consensusBN - Bayesian Network Fusion +[![CI](https://github.com/UCLM-SIMD/consensusBN/actions/workflows/ci.yml/badge.svg)](https://github.com/UCLM-SIMD/consensusBN/actions/workflows/ci.yml) + ![Java](https://img.shields.io/badge/Java-8%2B-blue) ![Maven](https://img.shields.io/badge/Maven-3.6%2B-orange) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) From 2cd719bbc58fd3b5adcf508a7fffe3490fdd6a00 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:10:21 +0200 Subject: [PATCH 26/32] Solving javadoc issues --- pom.xml | 20 +++ .../uclm/i3a/simd/consensusBN/AlphaOrder.java | 2 +- .../BackwardEquivalenceSearchDSep.java | 30 ++++- .../i3a/simd/consensusBN/BetaToAlpha.java | 64 +++++----- .../i3a/simd/consensusBN/ConsensusBES.java | 7 +- .../i3a/simd/consensusBN/ConsensusUnion.java | 4 + .../i3a/simd/consensusBN/DSeparationKey.java | 61 ++++++++- .../consensusBN/HeuristicConsensusBES.java | 27 +++- .../HeuristicConsensusMVoting.java | 4 +- .../consensusBN/PairWiseConsensusBES.java | 14 +-- .../uclm/i3a/simd/consensusBN/PowerSet.java | 2 +- .../uclm/i3a/simd/consensusBN/RandomBN.java | 2 +- .../es/uclm/i3a/simd/consensusBN/Utils.java | 117 +++++++++++++++--- .../i3a/simd/consensusBN/BetaToAlphaTest.java | 2 +- 14 files changed, 279 insertions(+), 77 deletions(-) diff --git a/pom.xml b/pom.xml index 0159f22..07b2317 100644 --- a/pom.xml +++ b/pom.xml @@ -179,6 +179,26 @@ report
+ + check + + check + + + + + PACKAGE + + + LINE + COVEREDRATIO + 0.50 + + + + + +
diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java b/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java index 35f99ac..aaf7b73 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/AlphaOrder.java @@ -83,7 +83,7 @@ private void checkExceptions(ArrayList setOfDags) { /** * Returns the nodes of the first DAG in the set, since all DAGs are assumed to have the same nodes. - * @return + * @return the nodes of the first DAG. */ public List getNodes(){ return(setOfDags.get(0).getNodes()); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java index 7c3a52f..d651022 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BackwardEquivalenceSearchDSep.java @@ -473,27 +473,53 @@ private double scoreGraphChangeDelete(Node x, Node y, Set conditioningSet) } /** * Returns the number of edges that were inserted during the consensus union and backward equivalence search process. - * @return + * @return The number of edges that were removed during the backward equivalence search process. */ public int getNumberOfRemovedEdges() { return this.numberOfRemovedEdges; } - + /** + * Sets the percentage threshold for edge deletion. + * This method allows the user to specify a percentage threshold for edge deletion, for an heuristic search. + * The percentage must be between 0.0 and 1.0, where 0.0 means no edges are deleted and 1.0 means all edges are considered for deletion. + * If the percentage is outside this range, an IllegalArgumentException is thrown. + * @param percentage The percentage threshold for edge deletion, must be between 0.0 and 1.0. + * @throws IllegalArgumentException if the percentage is not between 0.0 and 1.0. + */ public void setPercentage(double percentage) { if(percentage < 0.0 || percentage > 1.0) { throw new IllegalArgumentException("Percentage must be between 0.0 and 1.0"); } this.percentage = percentage; } + + /** + * Sets the maximum size of the conditioning set for edge deletion. + * This method allows the user to specify a maximum size for the conditioning set used in edge deletion for an heuristic search. + * The maximum size must be a non-negative integer. If it is negative, an IllegalArgumentException is thrown. + * @param maxSize The maximum size of the conditioning set. + */ public void setMaxSize(int maxSize) { if(maxSize < 0) { throw new IllegalArgumentException("Max size must be a non-negative integer"); } this.maxSize = maxSize; } + + /** + * Returns the percentage threshold for edge deletion. + * This method retrieves the current percentage threshold set for edge deletion. + * @return The percentage threshold for edge deletion. + */ public double getPercentage() { return this.percentage; } + + /** + * Returns the maximum size of the conditioning set for edge deletion. + * This method retrieves the current maximum size set for the conditioning set used in edge deletion. + * @return The maximum size of the conditioning set for edge deletion. + */ public int getMaxSize() { return this.maxSize; } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/BetaToAlpha.java b/src/main/java/es/uclm/i3a/simd/consensusBN/BetaToAlpha.java index 86e3856..2131af6 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/BetaToAlpha.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/BetaToAlpha.java @@ -20,7 +20,7 @@ public class BetaToAlpha { /** * The directed acyclic graph (DAG) to be transformed. */ - private final Dag G; + private final Dag dag; /** * The beta order derived from the alpha order. @@ -50,12 +50,12 @@ public class BetaToAlpha { /** * Constructor for BetaToAlpha that initializes the graph and alpha order. - * @param G the directed acyclic graph (DAG) to be transformed. + * @param dag the directed acyclic graph (DAG) to be transformed. * @param alpha the alpha order that the graph should respect. */ - public BetaToAlpha(Dag G, ArrayList alpha){ + public BetaToAlpha(Dag dag, ArrayList alpha){ this.alpha = alpha; - this.G = G; + this.dag = dag; this.beta = null; for(int i= 0; i< alpha.size(); i++){ Node n = alpha.get(i); @@ -66,12 +66,12 @@ public BetaToAlpha(Dag G, ArrayList alpha){ /** * Constructor for BetaToAlpha that initializes the graph without a specified alpha order. - * A random alpha order will need to be created. - * @param G + * A random alpha order will be created instead. + * @param dag the directed acyclic graph (DAG) to be transformed. */ - public BetaToAlpha(Dag G){ + public BetaToAlpha(Dag dag){ this.alpha = null; - this.G = G; + this.dag = dag; this.beta = null; } @@ -99,12 +99,12 @@ public void computeAlphaHash(){ /** * Builds a random alpha order from the nodes of the graph. This is used for test purposes to ensure that the transformation can handle different orders. - * @param aleatorio the random number generator to use for shuffling the nodes. + * @param randomGenerator the random number generator to use for shuffling the nodes. * @return a list of nodes representing a random alpha order. */ - public List randomAlfa (Random aleatorio){ - - List nodes = this.G.getNodes(); + public List randomAlpha (Random randomGenerator){ + + List nodes = this.dag.getNodes(); this.alpha = new ArrayList<>(); int[] index = new int[nodes.size()]; @@ -114,9 +114,8 @@ public List randomAlfa (Random aleatorio){ } for (int j = 0; j < nodes.size(); j++){ - - int indi = aleatorio.nextInt(nodes.size()); - int indj = aleatorio.nextInt(nodes.size()); + int indi = randomGenerator.nextInt(nodes.size()); + int indj = randomGenerator.nextInt(nodes.size()); int sw = index[indi]; index[indi] = index[indj]; index[indj] = sw; @@ -152,7 +151,7 @@ public void transform(){ * It also initializes the beta list with the first sink node and iteratively adds nodes to the beta order based on their relationships in the graph. */ private void buildBetaOrder() { - this.G_aux = new Dag(this.G); + this.G_aux = new Dag(this.dag); this.beta = new ArrayList<>(); List parents; @@ -181,7 +180,7 @@ private void buildBetaOrder() { for (; insertIndex < beta.size(); insertIndex++) { Node current = beta.get(insertIndex); if (alphaHash.get(current) > alphaHash.get(sink)) break; - if (G.getParents(current).contains(sink)) break; + if (dag.getParents(current).contains(sink)) break; } beta.add(insertIndex, sink); } @@ -238,23 +237,23 @@ private void transformWithBeta() { // Check if there is an edge from nodeZ to nodeY, if so, cover it. if ((nodeZ != null) && (this.alphaHash.get(nodeZ) > this.alphaHash.get(nodeY))){ - if(this.G.getEdge(nodeZ, nodeY) != null){ - List paZ = this.G.getParents(nodeZ); - List paY = this.G.getParents(nodeY); + if(this.dag.getEdge(nodeZ, nodeY) != null){ + List paZ = this.dag.getParents(nodeZ); + List paY = this.dag.getParents(nodeY); paY.remove(nodeZ); - this.G.removeEdge(nodeZ, nodeY); - this.G.addEdge(new Edge(nodeY,nodeZ,Endpoint.TAIL, Endpoint.ARROW)); + this.dag.removeEdge(nodeZ, nodeY); + this.dag.addEdge(new Edge(nodeY,nodeZ,Endpoint.TAIL, Endpoint.ARROW)); for(Node nodep: paZ){ - Edge pay = this.G.getEdge(nodep, nodeY); + Edge pay = this.dag.getEdge(nodep, nodeY); if(pay == null){ - this.G.addEdge(new Edge(nodep,nodeY,Endpoint.TAIL,Endpoint.ARROW)); + this.dag.addEdge(new Edge(nodep,nodeY,Endpoint.TAIL,Endpoint.ARROW)); this.numberOfInsertedEdges++; } } for(Node nodep : paY){ - Edge paz = this.G.getEdge(nodep,nodeZ); + Edge paz = this.dag.getEdge(nodep,nodeZ); if(paz == null){ - this.G.addEdge(new Edge(nodep,nodeZ,Endpoint.TAIL,Endpoint.ARROW)); + this.dag.addEdge(new Edge(nodep,nodeZ,Endpoint.TAIL,Endpoint.ARROW)); this.numberOfInsertedEdges++; } } @@ -271,7 +270,8 @@ private void transformWithBeta() { /** * Returns the number of edges that were inserted during the transformation process. * This method is useful for understanding how many modifications were made to the original graph to achieve the desired alpha order. - * @return + * @return the number of edges that were inserted during the transformation process. + * @see BetaToAlpha#transform() */ public int getNumberOfInsertedEdges(){ return this.numberOfInsertedEdges; @@ -282,16 +282,16 @@ public int getNumberOfInsertedEdges(){ * A sink node is defined as a node that does not have any children in the graph. * This method iterates through all nodes in the graph and checks their children to determine if they are sink nodes. * - * @param g the directed acyclic graph (DAG) from which to retrieve sink nodes. + * @param dagGraph the directed acyclic graph (DAG) from which to retrieve sink nodes. * @return an ArrayList of sink nodes that do not have any children in the graph. */ - private ArrayList getSinkNodes(Dag g){ + private ArrayList getSinkNodes(Dag dagGraph){ // Get nodes from DAG ArrayList sinkNodes = new ArrayList<>(); - List nodes = g.getNodes(); + List nodes = dagGraph.getNodes(); // Check which nodes don't have children and add them to sinkNodes for (Node node : nodes){ - if(g.getChildren(node).isEmpty()){ + if(dagGraph.getChildren(node).isEmpty()){ sinkNodes.add(node); } } @@ -336,7 +336,7 @@ public List getAlphaOrder() { * @return the transformed directed acyclic graph (DAG) as a Dag object. */ public Dag getGraph() { - return G; + return dag; } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java index 8d42e72..951566f 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusBES.java @@ -18,9 +18,8 @@ public class ConsensusBES implements Runnable { * Final output DAG after applying the Consensus Union and Backward Equivalence Search with D-separation. * This DAG represents the optimal fusion of the input DAGs. * It is computed by first merging the input DAGs into a consensus DAG and then refining it using the BES with D-separation. - * * @see ConsensusUnion - * @see BackwardEquivalenceSearchDSepTest + * @see BackwardEquivalenceSearchDSep */ protected Dag outputDag; @@ -104,7 +103,7 @@ public Dag getFusionDag(){ /** * Returns a valid ancestral order of the nodes in the fused DAG. - * @return + * @return a list of nodes representing an ancestral order of the resulting DAG. */ public List getOrderFusion(){ return this.getFusionDag().paths().getValidOrder(this.getFusionDag().getNodes(),true); @@ -112,7 +111,7 @@ public List getOrderFusion(){ /** * Returns the number of edges inserted during the consensus union and removed in the Backward Equivalence Search with D-separation. - * @return + * @return the number of edges inserted during the consensus union and removed in the Backward Equivalence Search with D-separation. */ public int getNumberOfInsertedEdges(){ return this.numberOfInsertedEdges; diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java index c0dd4ae..81a3b7e 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/ConsensusUnion.java @@ -163,6 +163,10 @@ public void run() { this.union = this.union(); } + /** + * Returns the list of transformed DAGs after applying the alpha order to the input DAGs with TransformDags. + * @return the list of transformed DAGs. + */ public ArrayList getTransformedDags() { if (this.imaps2alpha != null) { return this.imaps2alpha.getSetOfOutputDags(); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/DSeparationKey.java b/src/main/java/es/uclm/i3a/simd/consensusBN/DSeparationKey.java index 4e8cbb0..3c6d186 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/DSeparationKey.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/DSeparationKey.java @@ -7,11 +7,40 @@ import edu.cmu.tetrad.graph.Node; +/** + * This class represents a key for D-separation checks in a Bayesian network. + * It encapsulates two nodes (x and y) and a set of conditioning nodes. + * The key is used to efficiently check if two nodes are d-separated given a conditioning set. + * The equality and hashCode methods ensure that keys with the same nodes and conditioning set are treated as equal. + */ public class DSeparationKey { - private final Node y; + /** + * The node x in the D-separation key. + * This node is one of the two nodes being checked for d-separation. + */ private final Node x; + + /** + * The node y in the D-separation key. + * This node is the other node being checked for d-separation. + */ + private final Node y; + + /** + * The set of conditioning nodes in the D-separation key. + * This set contains nodes that are conditioned on when checking for d-separation between x and y. + * It is stored as a defensive copy to ensure immutability. + */ private final Set conditioningSet; + /** + * Constructor for DSeparationKey that initializes the key with two nodes and a set of conditioning nodes. + * The nodes x and y are stored in a consistent order to ensure that the key is symmetric. + * The conditioning set is stored as a defensive copy to prevent external modifications. + * @param x the first node in the D-separation key. + * @param y the second node in the D-separation key. + * @param conditioningSet the set of conditioning nodes in the D-separation key. + */ public DSeparationKey(Node x, Node y, Set conditioningSet) { // Since D-separation is symmetric, we ensure a consistent order for x and y if (x.getName().compareTo(y.getName()) <= 0) { @@ -24,6 +53,14 @@ public DSeparationKey(Node x, Node y, Set conditioningSet) { this.conditioningSet = new HashSet<>(conditioningSet); // copia defensiva } + /** + * Checks if this D-separation key is equal to another object. + * Two keys are considered equal if they have the same nodes (x and y) and + * the same set of conditioning nodes. + * The equality is symmetric, meaning the order of x and y does not matter. + * @param obj the object to compare with this D-separation key. + * @return true if the other object is a DSeparationKey with the same nodes and conditioning set, false otherwise. + */ @Override public boolean equals(Object obj) { if (this == obj) return true; @@ -35,22 +72,38 @@ public boolean equals(Object obj) { && conditioningSet.equals(other.conditioningSet); } + /** + * Returns the hash code for this D-separation key. + * The hash code is computed based on the nodes x and y, and the conditioning set. + * This ensures that two keys that are equal will have the same hash code. + * @return the hash code for this D-separation key. + */ @Override public int hashCode() { return Objects.hash(y, x, conditioningSet); } - + /** + * Returns the node y in the D-separation key. + * @return the node y in the D-separation key. + */ public Node getY() { return this.y; } - + /** + * Returns the node x in the D-separation key. + * @return the node x in the D-separation key. + */ public Node getX() { return this.x; } - + /** + * Returns the set of conditioning nodes in the D-separation key. + * This set is unmodifiable to prevent external modifications. + * @return the set of conditioning nodes in the D-separation key. + */ public Set getConditioningSet() { return Collections.unmodifiableSet(this.conditioningSet); } diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java index 93a8e41..cd0d4d0 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusBES.java @@ -4,15 +4,33 @@ import edu.cmu.tetrad.graph.Dag; +/** + * The {@code HeuristicConsensusBES} class extends the {@code ConsensusBES} class + * to implement a heuristic approach for backward equivalence search in directed acyclic graphs (DAGs). + *

+ * This class is designed to perform a consensus structure learning algorithm + * by applying a heuristic method for backward equivalence search (BES) with D-separation. + * It allows for a more efficient search by limiting the size of the conditioning set and applying a + * percentage threshold for determining d-separation between nodes. + *

+ * The constructor initializes the class with a list of input DAGs, a maximum size for the + * conditioning set, and a percentage threshold for d-separation. + * The `fusion` method applies the consensus union to compute a consensus DAG from the input DAGs, + * and then applies the backward equivalence search with D-separation with the specified parameters to refine the graph. + */ public class HeuristicConsensusBES extends ConsensusBES{ private final int maxSize; private final double percentage; /** - * Constructor for HeuristicConsensusBES2. + * Constructor for HeuristicConsensusBES. * This class extends ConsensusBES to implement a heuristic approach * for backward equivalence search in directed acyclic graphs (DAGs). + * + * @param dags the list of input DAGs to be fused. + * @param maxSize the maximum size of the conditioning set for d-separation checks. + * @param percentage the percentage/threshold for determining d-separation between nodes. */ public HeuristicConsensusBES(ArrayList dags, int maxSize, double percentage) { super(dags); @@ -20,7 +38,12 @@ public HeuristicConsensusBES(ArrayList dags, int maxSize, double percentage this.percentage = percentage; } - // Additional methods and overrides can be added here as needed + /** + * Executes the heuristic consensus backward equivalence search. + * This method first applies the ConsensusUnion to compute a consensus DAG from the input DAGs, + * and then applies the Backward Equivalence Search with D-separation to refine the graph, setting the maxSize and percentage parameters for an heuristic search. + * The resulting output DAG is stored in the outputDag attribute. + */ @Override public void fusion(){ // 1. Apply ConsensusUnion diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVoting.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVoting.java index caf1f11..f09ec62 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVoting.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HeuristicConsensusMVoting.java @@ -74,8 +74,8 @@ public class HeuristicConsensusMVoting { /** * Constructor for HeuristicConsensusMVoting. * Initializes the variables, output DAG, input DAGs, and weight matrix. - * @param setOfdags - * @param percentage + * @param setOfdags the list of input DAGs to be fused. + * @param percentage the percentage threshold for edge inclusion in the consensus DAG. */ public HeuristicConsensusMVoting(ArrayList setOfdags, double percentage) { this.variables = (ArrayList) setOfdags.get(0).getNodes(); diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBES.java b/src/main/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBES.java index 0fb563d..f1669bb 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBES.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/PairWiseConsensusBES.java @@ -48,8 +48,8 @@ public class PairWiseConsensusBES implements Runnable{ * Constructor for the PairWiseConsensusBES class. * It initializes the instance with two input DAGs and checks if they are valid. * If the input DAGs are not valid, it throws an IllegalArgumentException. - * @param firstDag - * @param secondDag + * @param firstDag the first input DAG for fusion similarity. + * @param secondDag the second input DAG for fusion similarity. */ public PairWiseConsensusBES(Dag firstDag, Dag secondDag) { checkInput(firstDag, secondDag); @@ -60,8 +60,8 @@ public PairWiseConsensusBES(Dag firstDag, Dag secondDag) { * Checks if the input DAGs are valid. * Validity is determined by ensuring that the DAGs are not null, contain at least one node and one edge, and have the same set of nodes. * If any of these conditions are not met, an IllegalArgumentException is thrown. - * @param firstDag first input DAG - * @param secondDag second input DAG + * @param firstDag first input DAG for fusion similarity + * @param secondDag second input DAG for fusion similarity * @throws IllegalArgumentException if the input DAGs are not valid */ private void checkInput(Dag firstDag, Dag secondDag) { @@ -103,7 +103,7 @@ public void fusion(){ * Returns the number of edges inserted during the fusion process. * This method retrieves the number of edges that were added to the consensus DAG during the fusion process. * It is useful for understanding how many edges were introduced in the consensus DAG compared to the original input DAGs. - * @return + * @return the number of edges inserted during the fusion process. */ public int getNumberOfInsertedEdges(){ return this.numberOfInsertedEdges; @@ -117,7 +117,7 @@ public int getNumberOfInsertedEdges(){ * * @see ConsensusBES#getUnion() * @see ConsensusBES#getNumberOfInsertedEdges() - * @return + * @return the number of edges in the union DAG after the consensus union process. */ public int getNumberOfUnionEdges(){ return this.numberOfUnionEdges; @@ -148,7 +148,7 @@ public int calculateHammingDistance(){ * It is useful for obtaining the consensus structure after the fusion process has been completed. * * @see ConsensusBES#getFusionDag() - * @return + * @return the resulting consensus DAG after applying the whole fusion process. */ public Dag getDagFusion(){ return this.consensusDAG; diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java index cfdd82e..c240f46 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/PowerSet.java @@ -116,7 +116,7 @@ public boolean hasMoreElements() { /** * Returns the next subset in the enumeration. - * @return The next subset as a Set. + * @return The next subset of the power set of nodes. */ @Override public Set nextElement() { diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/RandomBN.java b/src/main/java/es/uclm/i3a/simd/consensusBN/RandomBN.java index 5d91fd8..4840bcb 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/RandomBN.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/RandomBN.java @@ -13,11 +13,11 @@ import edu.cmu.tetrad.bayes.MlBayesIm.InitializationMethod; import edu.cmu.tetrad.data.*; -@Deprecated /** * RandomBN generates a set of random Bayesian networks (BNs) based on specified parameters. This class has been used for experiments, and is being maintained for compatibility with existing experiments. * Further development should avoid using this class and instead use @see RandomGraph from Tetrad for generating random DAGs. */ +@Deprecated public class RandomBN { int seed = 0; diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java index b8f7298..0c7d970 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/Utils.java @@ -17,24 +17,38 @@ import edu.cmu.tetrad.graph.GraphUtils; import edu.cmu.tetrad.graph.Node; -public class Utils { +/** + * Utility class providing static methods for graph operations, particularly for transforming PDAGs to DAGs and checking d-separation. + * This class includes methods for converting a PDAG to a DAG, checking if two nodes are d-separated in a DAG, and finding neighbors of nodes. + */ +public final class Utils { - /** - * pdagToDag Algorithm From Chickering 2002: - * "We first consider a simple implementation of PDAG-to-DAG due to Dor and Tarsi (1992). - * Let NX denote the neighbors of node X in a PDAG P. - * We first create a DAG G that contains all of the directed edges from P, and no other edges. - * We then repeat the following procedure: - * First, select a node X in P such that: - * (1) X has no out-going edges and - * (2) if NX is non-empty, then NX PaX is a clique. - * If P admits a consistent extension, the node X is guaranteed to exist. - * Next, for each undirected edge Y X incident to X in P, insert a directed edge Y X to G. - * Finally, remove X and all incident edges from the P and continue with the next node. - * The algorithm terminates when all nodes have been deleted from P." - * @param graph The graph to be transformed from PDAG to DAG. - */ + /** + * Transforms a PDAG (Partially Directed Acyclic Graph) into a DAG (Directed Acyclic Graph) + * using the algorithm proposed by Dor and Tarsi (1992), as presented in Chickering (2002). + * + *

The algorithm proceeds as follows: + *

    + *
  1. Let NX be the set of neighbors of node X in the PDAG P.
  2. + *
  3. Create a new DAG G containing all the directed edges from P (and no others).
  4. + *
  5. Iteratively repeat the following steps: + *
      + *
    1. Select a node X such that: + *
        + *
      • (1) X has no outgoing directed edges in P, and
      • + *
      • (2) if NX is non-empty, then NX ∪ Pa(X) forms a clique.
      • + *
      + * Such a node is guaranteed to exist if P admits a consistent extension.
    2. + *
    3. For each undirected edge Y—X incident to X in P, orient it as Y → X in G.
    4. + *
    5. Remove node X and all its incident edges from P.
    6. + *
    + *
  6. + *
  7. The algorithm terminates when all nodes have been removed from P.
  8. + *
+ * + * @param graph The input PDAG to be converted into a DAG. + */ public static void pdagToDag(Graph graph){ // First create a DAG G that contains all of the directed edges from the PDAG, and no other edges. Graph graphAux = new EdgeListGraph(graph); @@ -73,11 +87,29 @@ public static void pdagToDag(Graph graph){ }while(!nodes.isEmpty()); } - + /** + * Checks if two nodes in a DAG are d-separated given an empty set of conditioning nodes. + * @param g The DAG to check for d-separation. + * @param x The first node. + * @param y The second node. + * @return True if the nodes are d-separated, false otherwise. + */ public static boolean dSeparated(Dag g, Node x, Node y) { return dSeparated(g, x, y, new ArrayList<>()); } + /** + * Checks if two nodes in a DAG are d-separated given a set of conditioning nodes. + * This method uses a defensive copy of the conditioning set to ensure immutability. + * It builds an induced subgraph of the DAG containing only the relevant nodes and checks if there is a path between the two nodes that does not pass through the conditioning nodes. + * The method first finds the relevant nodes in the DAG, builds an induced subgraph, moralizes it, + * converts it to an undirected graph, and finally checks if the two nodes are reachable from each other in the undirected graph after removing the conditioning nodes. + * @param g The DAG to check for d-separation. + * @param x The first node. + * @param y The second node. + * @param cond The list of conditioning nodes. + * @return True if the nodes are d-separated, false otherwise. + */ public static boolean dSeparated(Dag g, Node x, Node y, List cond) { Set relevantNodes = findRelevantNodes(g, x, y, cond); @@ -88,6 +120,17 @@ public static boolean dSeparated(Dag g, Node x, Node y, List cond) { return !isReachable(aux, x, y); } + /** + * Finds the relevant nodes in the DAG that are needed to check d-separation between two nodes x and y, given a set of conditioning nodes. + * This method performs a depth-first search starting from nodes x and y, and includes all nodes that are reachable from either x or y, as well as the conditioning nodes. + * The method uses a stack to explore the graph and a set to keep track of visited nodes, ensuring that + * all relevant nodes are included in the final set. + * @param g The DAG in which to find the relevant nodes. + * @param x The first node. + * @param y The second node. + * @param cond The list of conditioning nodes. + * @return A set of nodes that are relevant for checking d-separation between x and y, including the conditioning nodes. + */ private static Set findRelevantNodes(Dag g, Node x, Node y, List cond) { Set visited = new HashSet<>(); Deque stack = new ArrayDeque<>(); @@ -108,6 +151,16 @@ private static Set findRelevantNodes(Dag g, Node x, Node y, List con return visited; } + /** + * Builds an induced subgraph from the given DAG containing only the nodes specified in the set {@code nodesToKeep}. + * The method creates a new graph that includes all nodes in {@code nodesToKeep} and all directed edges between them that exist in the original graph. + * It ensures that only directed edges are included, and undirected edges are ignored. + * @param g The original DAG from which to build the induced subgraph. + * @param nodesToKeep The set of nodes to include in the induced subgraph. + * This set should contain the nodes that are relevant for the d-separation check. + * @return A new graph representing the induced subgraph containing only the specified nodes and their directed edges. + * This graph is a subgraph of the original DAG, containing only the nodes in {@code nodesToKeep} and the directed edges between them that exist in the original graph. + */ private static Graph buildInducedSubgraph(Dag g, Set nodesToKeep) { Graph subgraph = new EdgeListGraph(); @@ -131,6 +184,13 @@ private static Graph buildInducedSubgraph(Dag g, Set nodesToKeep) { return subgraph; } + /** + * Moralizes the given graph by adding undirected edges between all pairs of parents of each child node. + * This process ensures that the resulting graph is undirected and that all parents of each child are connected. + * The moralization is done by iterating over each child node, retrieving its parents, and adding undirected edges between every pair of parents. + * This is a crucial step in preparing the graph for d-separation checks, as it ensures that the graph structure reflects the necessary connections between parents of child nodes. + * @param graph The graph to moralize. + */ private static void moralize(Graph graph) { for (Node child : graph.getNodes()) { List parents = graph.getParents(child); @@ -149,6 +209,13 @@ private static void moralize(Graph graph) { } } + /** + * Converts all directed edges in the graph to undirected edges. + * This method iterates through all edges in the graph and changes their endpoints to TAIL, + * effectively removing the directionality of the edges. This is useful for certain graph operations + * where the direction of edges is not relevant, such as when checking connectivity or performing undirected graph algorithms. + * @param graph The graph to convert. + */ private static void convertToUndirected(Graph graph) { for (Edge e : new ArrayList<>(graph.getEdges())) { if (e.isDirected()) { @@ -158,6 +225,18 @@ private static void convertToUndirected(Graph graph) { } } + /** + * Checks if there is a path between two nodes in the graph. + * This method performs a depth-first search starting from the {@code start} node and checks + * if it can reach the {@code target} node. It uses a stack to explore the graph and a set to keep track of visited nodes, + * ensuring that it does not revisit nodes. + * If the target node is found during the search, it returns true; otherwise, + * it returns false after exhausting all possible paths. + * @param g The graph to search. + * @param start The starting node. + * @param target The target node. + * @return True if there is a path from start to target, false otherwise. + */ private static boolean isReachable(Graph g, Node start, Node target) { Set visited = new HashSet<>(); Deque stack = new ArrayDeque<>(); @@ -179,7 +258,7 @@ private static boolean isReachable(Graph g, Node start, Node target) { } - /** + /** * Finds the nodes that are neighbors of node y and x in the graph. * This method retrieves the neighbors of node y that are also adjacent to node x, * ensuring that the edges between them are directed. @@ -204,6 +283,4 @@ public static List findNaYX(Node x, Node y, Graph graph) { return naYX; } - - } diff --git a/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java b/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java index 06504f7..e3900e4 100644 --- a/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java +++ b/src/test/java/es/uclm/i3a/simd/consensusBN/BetaToAlphaTest.java @@ -69,7 +69,7 @@ void testTransformRespectsAlphaOrder() { @Test void testRandomAlphaProducesPermutation() { BetaToAlpha bta = new BetaToAlpha(dag); - List randomAlpha = bta.randomAlfa(new Random(42)); + List randomAlpha = bta.randomAlpha(new Random(42)); assertNotNull(randomAlpha); assertEquals(dag.getNumNodes(), randomAlpha.size()); From 3a6fdd79c767a2a7cb17bb7259d436b0cb66de54 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:33:11 +0200 Subject: [PATCH 27/32] Adding coverage check and bn_fusion.jpg --- .github/workflows/ci.yml | 8 ++++---- README.md | 3 +-- assets/bn_fusion.jpg | Bin 0 -> 188398 bytes pom.xml | 7 +++++-- 4 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 assets/bn_fusion.jpg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 173acca..356a5d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,8 +17,8 @@ jobs: - name: Set up Java uses: actions/setup-java@v4 with: - distribution: 'temurin' # puedes cambiarlo por zulu, adopt, etc. - java-version: '17' # cambia a 11 o el que uses en tu proyecto + distribution: 'temurin' + java-version: '17' - name: Cache Maven packages uses: actions/cache@v4 @@ -28,5 +28,5 @@ jobs: restore-keys: | ${{ runner.os }}-maven- - - name: Build and Test with Maven - run: mvn clean test + - name: Build, Test, and Check Coverage with Maven (80% coverage to pass) + run: mvn clean verify \ No newline at end of file diff --git a/README.md b/README.md index b88878d..bd873ec 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # consensusBN - Bayesian Network Fusion [![CI](https://github.com/UCLM-SIMD/consensusBN/actions/workflows/ci.yml/badge.svg)](https://github.com/UCLM-SIMD/consensusBN/actions/workflows/ci.yml) - ![Java](https://img.shields.io/badge/Java-8%2B-blue) ![Maven](https://img.shields.io/badge/Maven-3.6%2B-orange) [![License](https://img.shields.io/badge/license-MIT-green)](LICENSE) @@ -10,7 +9,7 @@ `consensusBN` is a Java-based library for Bayesian Network Fusion. This project allows users to combine multiple Bayesian networks into a single consensus network, leveraging the power of consensus-based modeling techniques. The project is supported by a published paper [(link)](https://www.sciencedirect.com/science/article/abs/pii/S156625352030364X), titled "Efficient and accurate structural fusion of Bayesian networks." -![Bayesian Network Fusion](assets/bn_fusion.png) +![Bayesian Network Fusion](assets/bn_fusion.jpg) ## Features diff --git a/assets/bn_fusion.jpg b/assets/bn_fusion.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e7a2b52595fbebabd1134aaef21d0b9313f82371 GIT binary patch literal 188398 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⁢a$7# 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

}s@*lmU?YZ#bg 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%0Ano)OOUfao{b!CXb$x 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 literal 0 HcmV?d00001 diff --git a/pom.xml b/pom.xml index 07b2317..216c233 100644 --- a/pom.xml +++ b/pom.xml @@ -174,10 +174,13 @@ report - test + verify report + + ${project.build.directory}/jacoco-report + check @@ -192,7 +195,7 @@ LINE COVEREDRATIO - 0.50 + 0.80 From ec20d5f9c3ee1d95b86c571ba763c0a3f337eda7 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Tue, 29 Jul 2025 13:36:23 +0200 Subject: [PATCH 28/32] Adding basic tests for HierarchicalAgglomerativeClusterBNS --- ...HierarchicalAgglomerativeClustererBNs.java | 7 +- ...archicalAgglomerativeClustererBNsTest.java | 112 ++++++++++++++++++ 2 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/test/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNsTest.java diff --git a/src/main/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNs.java b/src/main/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNs.java index dee7bb2..e55c0d9 100644 --- a/src/main/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNs.java +++ b/src/main/java/es/uclm/i3a/simd/consensusBN/HierarchicalAgglomerativeClustererBNs.java @@ -106,7 +106,7 @@ public int cluster() { clustersIndexes[i][i][0] = true; } - dissimilarityMatrix = computeDissimilarityMatrix(); + computeDissimilarityMatrix(); for (int a = 1; a inputDags; + + @BeforeEach + public void setUp() { + int numVariables = 4; // Number of variables in the DAGs + int numDags = 10; // Number of DAGs to generate + int maxEdges = 6; // Maximum number of edges in each DAG + int maxInDegree = 2; // Maximum in-degree for each node + int maxOutDegree = 2; // Maximum out-degree for each node + int maxDegree = 3; // Maximum degree for each node + boolean connected = true; // Whether the DAGs should be connected + long seed = 42; // Seed for random number generation + + // Generate a list of random DAGs using GraphTestHelper + inputDags = new ArrayList<>(); + inputDags.addAll(GraphTestHelper.generateRandomDagList(numVariables, numDags, maxEdges, maxInDegree, maxOutDegree, maxDegree, connected, seed)); + } + + + @Test + public void testConstructorAndGetSetOfBNs() { + + HierarchicalAgglomerativeClustererBNs clusterer = new HierarchicalAgglomerativeClustererBNs(inputDags, 2); + + assertEquals(inputDags.size(), clusterer.getSetOfBNs().size()); + assertEquals(inputDags, clusterer.getSetOfBNs()); + } + + @Test + public void testClusterStopsEarlyDueToMaxSize() { + + HierarchicalAgglomerativeClustererBNs clusterer = new HierarchicalAgglomerativeClustererBNs(inputDags, 1); + int numDagsAfterCluster = clusterer.cluster(); + + // Only one fusion should occur since maxSize is 1 + assertEquals((int)inputDags.size()/2, numDagsAfterCluster); + } + + @Test + public void testGetClustersOutputAtLevelZero() { + + HierarchicalAgglomerativeClustererBNs clusterer = new HierarchicalAgglomerativeClustererBNs(inputDags, 2); + clusterer.cluster(); + + ArrayList output = clusterer.getClustersOutput(0); + assertEquals(inputDags.size(), output.size(), "En el nivel 0 debe haber tantos DAGs como en la entrada"); + } + + @Test + public void testGetInsertedEdges() { + HierarchicalAgglomerativeClustererBNs clusterer = new HierarchicalAgglomerativeClustererBNs(inputDags, 2); + clusterer.cluster(); + + int insertedEdges = clusterer.getInsertedEdges(1); + assertTrue(insertedEdges >= 0, "Debe haber al menos 0 enlaces insertadas en el nivel 1"); + } + + @Test + public void testComputeConsensusDag() { + HierarchicalAgglomerativeClustererBNs clusterer = new HierarchicalAgglomerativeClustererBNs(inputDags, 2); + int level = clusterer.cluster(); + + Dag consensus = clusterer.computeConsensusDag(level); + assertNotNull(consensus, "El DAG de consenso no debería ser null"); + } + + + + /* + @Test + public void testFullClusteringUntilOneCluster() { + // Sin restricciones de tamaño ni complejidad + HierarchicalAgglomerativeClustererBNs clusterer = new HierarchicalAgglomerativeClustererBNs(inputDags, 0.0); + int result = clusterer.cluster(); + + // Debe haber n-1 fusiones si todo fue bien, por lo que al final solo debe quedar un cluster + assertEquals(1, result, "Deberían haberse hecho n-1 fusiones"); + + ArrayList resultClusters = clusterer.getClustersOutput(result); + assertEquals(1, resultClusters.size(), "Al final solo debe quedar un cluster"); + } + + @Test + public void testClusteringStopsDueToComplexity() { + ArrayList dags = new ArrayList<>(); + dags.addAll(GraphTestHelper.generateRandomDagList(10, 2, 30, 10, 10, 10, true, 42)); + + // Muy bajo el umbral de complejidad para forzar que no se fusionen + HierarchicalAgglomerativeClustererBNs clusterer = new HierarchicalAgglomerativeClustererBNs(dags, 0.01); + int result = clusterer.cluster(); + + assertTrue(result <= 1, "El clustering debería detenerse porque los DAGs fusionados son muy complejos"); + } + */ + +} + + From 53603f74c135365e8e0b0067fc475d98777f6944 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:27:25 +0200 Subject: [PATCH 29/32] Adding Maven Central Repository and Github Packages workflow --- .github/workflows/maven-publish.yml | 46 +++++++++++++++++++++++++++++ pom.xml | 41 +++++++++++++++++++++---- 2 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/maven-publish.yml diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml new file mode 100644 index 0000000..1ea1dcf --- /dev/null +++ b/.github/workflows/maven-publish.yml @@ -0,0 +1,46 @@ +# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created +# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path + +name: Maven Package + +on: + release: + types: [created] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Build with Maven + run: mvn -B package --file pom.xml + + - name: Publish to GitHub Packages Apache Maven + run: mvn deploy + env: + GITHUB_TOKEN: ${{ github.token }} # GITHUB_TOKEN is the default env for the password + + - name: Set up Apache Maven Central + uses: actions/setup-java@v4 + with: # running setup-java again overwrites the settings.xml + distribution: 'temurin' + java-version: '17' + server-id: maven # Value of the distributionManagement/repository/id field of the pom.xml + server-username: MAVEN_USERNAME # env variable for username in deploy + server-password: MAVEN_CENTRAL_TOKEN # env variable for token in deploy + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} # Value of the GPG private key to import + gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase + + - name: Publish to Apache Maven Central + run: mvn deploy + env: + MAVEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 216c233..1c42e66 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 - org.albacete.simd + io.github.jlaborda consensusBN 1.0.0 jar @@ -106,17 +106,32 @@ + + + org.apache.maven.plugins + maven-gpg-plugin + 3.1.0 + + + sign-artifacts + verify + + sign + + + + + org.apache.maven.plugins maven-source-plugin - 3.3.0 + 3.2.1 attach-sources - verify - jar-no-fork + jar @@ -126,7 +141,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.2.0 + 3.3.1 attach-javadocs @@ -205,17 +220,31 @@ + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + true + + + + From 5cbb4217862232f1f5810f79d589afd6d1656a40 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:33:23 +0200 Subject: [PATCH 30/32] Adding gpg secrets --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 356a5d8..2560821 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,8 @@ jobs: with: distribution: 'temurin' java-version: '17' + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} - name: Cache Maven packages uses: actions/cache@v4 From c000ba73e61cec0b44f0b685c9467816e0e34989 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:45:34 +0200 Subject: [PATCH 31/32] Adding loopback in pinetry mode for gpg passphrase usage --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index 1c42e66..131401d 100644 --- a/pom.xml +++ b/pom.xml @@ -120,6 +120,12 @@ + + + --pinentry-mode + loopback + + From 3c9df75b29854816cbacd1986ec594ed7d671548 Mon Sep 17 00:00:00 2001 From: JLaborda <15078416+JLaborda@users.noreply.github.com> Date: Thu, 31 Jul 2025 12:58:33 +0200 Subject: [PATCH 32/32] Adding passphare into command line --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2560821..9afb123 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,4 +31,4 @@ jobs: ${{ runner.os }}-maven- - name: Build, Test, and Check Coverage with Maven (80% coverage to pass) - run: mvn clean verify \ No newline at end of file + run: mvn clean verify -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} \ No newline at end of file