edu.cmu.tetrad.search.utils.FciOrient Maven / Gradle / Ivy
///////////////////////////////////////////////////////////////////////////////
// For information as to what this class does, see the Javadoc, below. //
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, //
// 2007, 2008, 2009, 2010, 2014, 2015, 2022 by Peter Spirtes, Richard //
// Scheines, Joseph Ramsey, and Clark Glymour. //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation; either version 2 of the License, or //
// (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY; without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU General Public License for more details. //
// //
// You should have received a copy of the GNU General Public License //
// along with this program; if not, write to the Free Software //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //
///////////////////////////////////////////////////////////////////////////////
package edu.cmu.tetrad.search.utils;
import edu.cmu.tetrad.data.Knowledge;
import edu.cmu.tetrad.data.KnowledgeEdge;
import edu.cmu.tetrad.graph.*;
import edu.cmu.tetrad.search.Fci;
import edu.cmu.tetrad.search.GFci;
import edu.cmu.tetrad.search.Rfci;
import edu.cmu.tetrad.util.ChoiceGenerator;
import edu.cmu.tetrad.util.TetradLogger;
import java.util.*;
/**
* Performs the final orientation steps of the FCI algorithms, which
* is a useful tool to use in a variety of FCI-like algorithms.
*
* There are two versions of these final orientation steps, one due to
* Peter Spirtes (the original, in Causation, Prediction and Search), which is arrow complete, and the other which Jiji
* Zhang worked out in his Ph.D. dissertation, which is both arrow and tail complete. The references for these are as
* follows.
*
* Spirtes, P., Glymour, C. N., Scheines, R., & Heckerman, D. (2000).
* Causation, prediction, and search. MIT press.
*
* Zhang, J. (2008). On the completeness of orientation rules for causal
* discovery in the presence of latent confounders and selection bias. Artificial Intelligence, 172(16-17),
* 1873-1896.
*
* These final rules are used in all algorithms in Tetrad that
* follow and refine the FCI algorithm--for example, the GFCI and RFCI algorihtms.
*
* We've made the methods for each of the separate rules publicly
* accessible in case someone wants to use the individual rules in the context of their own algorithms.
*
* @author Erin Korber, June 2004
* @author Alex Smith, December 2008
* @author josephramsey
* @author Choh-Man Teng
* @see Fci
* @see GFci
* @see Rfci
*/
public final class FciOrient {
private final SepsetProducer sepsets;
private final TetradLogger logger = TetradLogger.getInstance();
private Knowledge knowledge = new Knowledge();
private boolean changeFlag = true;
private boolean completeRuleSetUsed = true;
private int maxPathLength = -1;
private boolean verbose;
private Graph truePag;
private boolean doDiscriminatingPathColliderRule = true;
private boolean doDiscriminatingPathTailRule = true;
/**
* Constructs a new FCI search for the given independence test and background knowledge.
*/
public FciOrient(SepsetProducer sepsets) {
this.sepsets = sepsets;
}
/**
* Gets a list of every uncovered partially directed path between two nodes in the graph.
*
* Probably extremely slow.
*
* @param n1 The beginning node of the undirectedPaths.
* @param n2 The ending node of the undirectedPaths.
* @return A list of uncovered partially directed undirectedPaths from n1 to n2.
*/
public static List> getUcPdPaths(Node n1, Node n2, Graph graph) {
List> ucPdPaths = new LinkedList<>();
LinkedList soFar = new LinkedList<>();
soFar.add(n1);
List adjacencies = graph.getAdjacentNodes(n1);
for (Node curr : adjacencies) {
getUcPdPsHelper(curr, soFar, n2, ucPdPaths, graph);
}
return ucPdPaths;
}
/**
* Used in getUcPdPaths(n1,n2) to perform a breadth-first search on the graph.
*
* ASSUMES soFar CONTAINS AT LEAST ONE NODE!
*
* Probably extremely slow.
*
* @param curr The getModel node to test for addition.
* @param soFar The getModel partially built-up path.
* @param end The node to finish the undirectedPaths at.
* @param ucPdPaths The getModel list of uncovered p.d. undirectedPaths.
*/
private static void getUcPdPsHelper(Node curr, List soFar, Node end,
List> ucPdPaths, Graph graph) {
if (soFar.contains(curr)) {
return;
}
Node prev = soFar.get(soFar.size() - 1);
if (graph.getEndpoint(prev, curr) == Endpoint.TAIL
|| graph.getEndpoint(curr, prev) == Endpoint.ARROW) {
return; // Adding curr would make soFar not p.d.
} else if (soFar.size() >= 2) {
Node prev2 = soFar.get(soFar.size() - 2);
if (graph.isAdjacentTo(prev2, curr)) {
return; // Adding curr would make soFar not uncovered.
}
}
soFar.add(curr); // Adding curr is OK, so let's do it.
if (curr.equals(end)) {
// We've reached the goal! Save soFar as a path.
ucPdPaths.add(new LinkedList<>(soFar));
} else {
// Otherwise, try each node adjacent to the getModel one.
List adjacents = graph.getAdjacentNodes(curr);
for (Node next : adjacents) {
getUcPdPsHelper(next, soFar, end, ucPdPaths, graph);
}
}
soFar.remove(soFar.get(soFar.size() - 1)); // For other recursive calls.
}
/**
* Gets a list of every uncovered circle path between two nodes in the graph by iterating through the uncovered
* partially directed undirectedPaths and only keeping the circle undirectedPaths.
*
* Probably extremely slow.
*
* @param n1 The beginning node of the undirectedPaths.
* @param n2 The ending node of the undirectedPaths.
* @return A list of uncovered circle undirectedPaths between n1 and n2.
*/
public static List> getUcCirclePaths(Node n1, Node n2, Graph graph) {
List> ucCirclePaths = new LinkedList<>();
List> ucPdPaths = getUcPdPaths(n1, n2, graph);
for (List path : ucPdPaths) {
for (int i = 0; i < path.size() - 1; i++) {
Node j = path.get(i);
Node sj = path.get(i + 1);
if (!(graph.getEndpoint(j, sj) == Endpoint.CIRCLE)) {
break;
}
if (!(graph.getEndpoint(sj, j) == Endpoint.CIRCLE)) {
break;
}
// This edge is OK, it's all circles.
if (i == path.size() - 2) {
// We're at the last edge, so this is a circle path.
ucCirclePaths.add(path);
}
}
}
return ucCirclePaths;
}
public static boolean isArrowheadAllowed(Node x, Node y, Graph graph, Knowledge knowledge) {
if (!graph.isAdjacentTo(x, y)) return false;
if (graph.getEndpoint(x, y) == Endpoint.ARROW) {
return true;
}
if (graph.getEndpoint(x, y) == Endpoint.TAIL) {
return false;
}
if (graph.getEndpoint(y, x) == Endpoint.ARROW && graph.getEndpoint(x, y) == Endpoint.CIRCLE) {
if (knowledge.isForbidden(x.getName(), y.getName())) {
return true;
}
}
if (graph.getEndpoint(y, x) == Endpoint.TAIL && graph.getEndpoint(x, y) == Endpoint.CIRCLE) {
if (knowledge.isForbidden(x.getName(), y.getName())) {
return false;
}
}
return graph.getEndpoint(x, y) == Endpoint.CIRCLE;
}
/**
* Performs final FCI orientation on the given graph.
*
* @param graph The graph to further orient.
* @return The oriented graph.
*/
public Graph orient(Graph graph) {
this.logger.forceLogMessage("Starting FCI algorithm.");
ruleR0(graph);
if (this.verbose) {
logger.forceLogMessage("R0");
}
// Step CI D. (Zhang's step F4.)
doFinalOrientation(graph);
if (this.verbose) {
this.logger.forceLogMessage("Returning graph: " + graph);
}
return graph;
}
/**
* Returns the map from {x,y} to {z1,...,zn} for x _||_ y | z1,..,zn.
*
* @return Thia map.
*/
public SepsetProducer getSepsets() {
return this.sepsets;
}
/**
* Sets the knowledge to use for the final orientation.
*
* @param knowledge This knowledge.
*/
public void setKnowledge(Knowledge knowledge) {
if (knowledge == null) {
throw new NullPointerException();
}
this.knowledge = new Knowledge(knowledge);
}
/**
* @return true if Zhang's complete rule set should be used, false if only R1-R4 (the rule set of the original FCI)
* should be used. False by default.
*/
public boolean isCompleteRuleSetUsed() {
return this.completeRuleSetUsed;
}
/**
* @param completeRuleSetUsed set to true if Zhang's complete rule set should be used, false if only R1-R4 (the rule
* set of the original FCI) should be used. False by default.
*/
public void setCompleteRuleSetUsed(boolean completeRuleSetUsed) {
this.completeRuleSetUsed = completeRuleSetUsed;
}
/**
* Orients colliders in the graph. (FCI Step C)
*
* Zhang's step F3, rule R0.
*
* @param graph The graph to orient.
*/
public void ruleR0(Graph graph) {
graph.reorientAllWith(Endpoint.CIRCLE);
fciOrientbk(this.knowledge, graph, graph.getNodes());
List nodes = graph.getNodes();
for (Node b : nodes) {
if (Thread.currentThread().isInterrupted()) {
break;
}
List adjacentNodes = new ArrayList<>(graph.getAdjacentNodes(b));
if (adjacentNodes.size() < 2) {
continue;
}
ChoiceGenerator cg = new ChoiceGenerator(adjacentNodes.size(), 2);
int[] combination;
while ((combination = cg.next()) != null) {
if (Thread.currentThread().isInterrupted()) {
break;
}
Node a = adjacentNodes.get(combination[0]);
Node c = adjacentNodes.get(combination[1]);
// Skip triples that are shielded.
if (graph.isAdjacentTo(a, c)) {
continue;
}
if (graph.isDefCollider(a, b, c)) {
continue;
}
if (this.sepsets.isUnshieldedCollider(a, b, c)) {
if (!isArrowheadAllowed(a, b, graph, knowledge)) {
continue;
}
if (!isArrowheadAllowed(c, b, graph, knowledge)) {
continue;
}
graph.setEndpoint(a, b, Endpoint.ARROW);
graph.setEndpoint(c, b, Endpoint.ARROW);
if (this.verbose) {
this.logger.forceLogMessage(LogUtilsSearch.colliderOrientedMsg(a, b, c));
printWrongColliderMessage(a, b, c, graph);
}
}
}
}
}
/**
* Orients the graph according to rules in the graph (FCI step D).
*
* Zhang's step F4, rules R1-R10.
*/
public void doFinalOrientation(Graph graph) {
if (this.completeRuleSetUsed) {
zhangFinalOrientation(graph);
} else {
spirtesFinalOrientation(graph);
}
}
public void spirtesFinalOrientation(Graph graph) {
this.changeFlag = true;
boolean firstTime = true;
while (this.changeFlag) {
if (Thread.currentThread().isInterrupted()) {
break;
}
this.changeFlag = false;
rulesR1R2cycle(graph);
ruleR3(graph);
// R4 requires an arrow orientation.
if (this.changeFlag || (firstTime && !this.knowledge.isEmpty())) {
ruleR4B(graph);
firstTime = false;
}
if (this.verbose) {
logger.forceLogMessage("Epoch");
}
}
}
public void zhangFinalOrientation(Graph graph) {
this.changeFlag = true;
boolean firstTime = true;
while (this.changeFlag && !Thread.currentThread().isInterrupted()) {
this.changeFlag = false;
rulesR1R2cycle(graph);
ruleR3(graph);
// R4 requires an arrow orientation.
if (this.changeFlag || (firstTime && !this.knowledge.isEmpty())) {
ruleR4B(graph);
firstTime = false;
}
if (this.verbose) {
logger.forceLogMessage("Epoch");
}
}
if (isCompleteRuleSetUsed()) {
// Now, by a remark on page 100 of Zhang's dissertation, we apply rule
// R5 once.
ruleR5(graph);
// Now, by a further remark on page 102, we apply R6,R7 as many times
// as possible.
this.changeFlag = true;
while (this.changeFlag && !Thread.currentThread().isInterrupted()) {
this.changeFlag = false;
ruleR6R7(graph);
}
// Finally, we apply R8-R10 as many times as possible.
this.changeFlag = true;
while (this.changeFlag && !Thread.currentThread().isInterrupted()) {
this.changeFlag = false;
rulesR8R9R10(graph);
}
}
}
//Does all 3 of these rules at once instead of going through all
// triples multiple times per iteration of doFinalOrientation.
public void rulesR1R2cycle(Graph graph) {
List nodes = graph.getNodes();
for (Node B : nodes) {
if (Thread.currentThread().isInterrupted()) {
break;
}
List adj = new ArrayList<>(graph.getAdjacentNodes(B));
if (adj.size() < 2) {
continue;
}
ChoiceGenerator cg = new ChoiceGenerator(adj.size(), 2);
int[] combination;
while ((combination = cg.next()) != null && !Thread.currentThread().isInterrupted()) {
Node A = adj.get(combination[0]);
Node C = adj.get(combination[1]);
//choice gen doesnt do diff orders, so must switch A & C around.
ruleR1(A, B, C, graph);
ruleR1(C, B, A, graph);
ruleR2(A, B, C, graph);
ruleR2(C, B, A, graph);
}
}
}
/// R1, away from collider
// If a*->bo-*c and a, c not adjacent then a*->b->c
public void ruleR1(Node a, Node b, Node c, Graph graph) {
if (graph.isAdjacentTo(a, c)) {
return;
}
if (graph.getEndpoint(a, b) == Endpoint.ARROW && graph.getEndpoint(c, b) == Endpoint.CIRCLE) {
if (!isArrowheadAllowed(b, c, graph, knowledge)) {
return;
}
graph.setEndpoint(c, b, Endpoint.TAIL);
graph.setEndpoint(b, c, Endpoint.ARROW);
this.changeFlag = true;
if (this.verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("R1: Away from collider", graph.getEdge(b, c)));
}
}
}
//if a*-oc and either a-->b*->c or a*->b-->c, and a*-oc then a*->c
// This is Zhang's rule R2.
public void ruleR2(Node a, Node b, Node c, Graph graph) {
if ((graph.isAdjacentTo(a, c)) && (graph.getEndpoint(a, c) == Endpoint.CIRCLE)) {
if ((graph.getEndpoint(a, b) == Endpoint.ARROW && graph.getEndpoint(b, c) == Endpoint.ARROW)
&& (graph.getEndpoint(b, a) == Endpoint.TAIL || graph.getEndpoint(c, b) == Endpoint.TAIL)) {
if (!isArrowheadAllowed(a, c, graph, knowledge)) {
return;
}
graph.setEndpoint(a, c, Endpoint.ARROW);
if (this.verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("R2: Away from ancestor", graph.getEdge(a, c)));
}
this.changeFlag = true;
}
}
}
/**
* Implements the double-triangle orientation rule, which states that if D*-oB, A*->B<-*C and A*-oDo-*C, and !adj(a,
* c), D*-oB, then D*->B.
*
* This is Zhang's rule R3.
*/
public void ruleR3(Graph graph) {
List nodes = graph.getNodes();
for (Node b : nodes) {
if (Thread.currentThread().isInterrupted()) {
break;
}
List intoBArrows = graph.getNodesInTo(b, Endpoint.ARROW);
if (intoBArrows.size() < 2) continue;
ChoiceGenerator gen = new ChoiceGenerator(intoBArrows.size(), 2);
int[] choice;
while ((choice = gen.next()) != null) {
List B = GraphUtils.asList(choice, intoBArrows);
Node a = B.get(0);
Node c = B.get(1);
List adj = new ArrayList<>(graph.getAdjacentNodes(a));
adj.retainAll(graph.getAdjacentNodes(c));
for (Node d : adj) {
if (d == a) continue;
if (graph.getEndpoint(a, d) == Endpoint.CIRCLE && graph.getEndpoint(c, d) == Endpoint.CIRCLE) {
if (!graph.isAdjacentTo(a, c)) {
if (graph.getEndpoint(d, b) == Endpoint.CIRCLE) {
if (!isArrowheadAllowed(d, b, graph, knowledge)) {
return;
}
graph.setEndpoint(d, b, Endpoint.ARROW);
if (this.verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("R3: Double triangle", graph.getEdge(d, b)));
}
this.changeFlag = true;
}
}
}
}
}
}
}
/**
* The triangles that must be oriented this way (won't be done by another rule) all look like the ones below, where
* the dots are a collider path from L to A with each node on the path (except L) a parent of C.
*
* B
* xo x is either an arrowhead or a circle
* / \
* v v
* L....A --> C
*
*
* This is Zhang's rule R4, discriminating paths.
*/
public void ruleR4B(Graph graph) {
if (doDiscriminatingPathColliderRule || doDiscriminatingPathTailRule) {
List nodes = graph.getNodes();
for (Node b : nodes) {
if (Thread.currentThread().isInterrupted()) {
break;
}
// potential A and C candidate pairs are only those
// that look like this: A<-*Bo-*C
List possA = graph.getNodesOutTo(b, Endpoint.ARROW);
List possC = graph.getNodesInTo(b, Endpoint.CIRCLE);
for (Node a : possA) {
if (Thread.currentThread().isInterrupted()) {
break;
}
for (Node c : possC) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (a == c) continue;
if (!graph.isParentOf(a, c)) {
continue;
}
if (graph.getEndpoint(b, c) != Endpoint.ARROW) {
continue;
}
ddpOrient(a, b, c, graph);
}
}
}
}
}
/**
* a method to search "back from a" to find a DDP. It is called with a reachability list (first consisting only of
* a). This is breadth-first, utilizing "reachability" concept from Geiger, Verma, and Pearl 1990. The body of a DDP
* consists of colliders that are parents of c.
*/
public void ddpOrient(Node a, Node b, Node c, Graph graph) {
Queue Q = new ArrayDeque<>(20);
Set V = new HashSet<>();
Node e = null;
int distance = 0;
Map previous = new HashMap<>();
List cParents = graph.getParents(c);
Q.offer(a);
V.add(a);
V.add(b);
previous.put(a, b);
while (!Q.isEmpty()) {
if (Thread.currentThread().isInterrupted()) {
break;
}
Node t = Q.poll();
if (e == null || e == t) {
e = t;
distance++;
if (distance > 0 && distance > (this.maxPathLength == -1 ? 1000 : this.maxPathLength)) {
return;
}
}
List nodesInTo = graph.getNodesInTo(t, Endpoint.ARROW);
for (Node d : nodesInTo) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (V.contains(d)) {
continue;
}
previous.put(d, t);
Node p = previous.get(t);
if (!graph.isDefCollider(d, t, p)) {
continue;
}
previous.put(d, t);
if (!graph.isAdjacentTo(d, c)) {
if (doDdpOrientation(d, a, b, c, graph)) {
return;
}
}
if (cParents.contains(d)) {
Q.offer(d);
V.add(d);
}
}
}
}
/**
* Implements Zhang's rule R5, orient circle undirectedPaths: for any Ao-oB, if there is an uncovered circle path u
* = [A,C,...,D,B] such that A,D nonadjacent and B,C nonadjacent, then A---B and orient every edge on u undirected.
*/
public void ruleR5(Graph graph) {
List nodes = graph.getNodes();
for (Node a : nodes) {
if (Thread.currentThread().isInterrupted()) {
break;
}
List adjacents = graph.getNodesInTo(a, Endpoint.CIRCLE);
for (Node b : adjacents) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (!(graph.getEndpoint(a, b) == Endpoint.CIRCLE)) {
continue;
}
// We know Ao-oB.
List> ucCirclePaths = getUcCirclePaths(a, b, graph);
for (List u : ucCirclePaths) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (u.size() < 3) {
continue;
}
Node c = u.get(1);
Node d = u.get(u.size() - 2);
if (graph.isAdjacentTo(a, d)) {
continue;
}
if (graph.isAdjacentTo(b, c)) {
continue;
}
// We know u is as required: R5 applies!
graph.setEndpoint(a, b, Endpoint.TAIL);
graph.setEndpoint(b, a, Endpoint.TAIL);
if (verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg(
"R5: Orient circle path", graph.getEdge(a, b)));
}
orientTailPath(u, graph);
this.changeFlag = true;
}
}
}
}
/**
* Implements Zhang's rules R6 and R7, applies them over the graph once. Orient single tails. R6: If A---Bo-*C then
* A---B--*C. R7: If A--oBo-*C and A,C nonadjacent, then A--oB--*C
*/
public void ruleR6R7(Graph graph) {
List nodes = graph.getNodes();
for (Node b : nodes) {
if (Thread.currentThread().isInterrupted()) {
break;
}
List adjacents = new ArrayList<>(graph.getAdjacentNodes(b));
if (adjacents.size() < 2) {
continue;
}
ChoiceGenerator cg = new ChoiceGenerator(adjacents.size(), 2);
for (int[] choice = cg.next(); choice != null && !Thread.currentThread().isInterrupted(); choice = cg.next()) {
Node a = adjacents.get(choice[0]);
Node c = adjacents.get(choice[1]);
if (graph.isAdjacentTo(a, c)) {
continue;
}
if (!(graph.getEndpoint(b, a) == Endpoint.TAIL)) {
continue;
}
if (!(graph.getEndpoint(c, b) == Endpoint.CIRCLE)) {
continue;
}
// We know A--*Bo-*C.
if (graph.getEndpoint(a, b) == Endpoint.TAIL) {
// We know A---Bo-*C: R6 applies!
graph.setEndpoint(c, b, Endpoint.TAIL);
if (verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg(
"R6: Single tails (tail)", graph.getEdge(c, b)));
}
this.changeFlag = true;
}
if (graph.getEndpoint(a, b) == Endpoint.CIRCLE) {
// if (graph.isAdjacentTo(a, c)) continue;
graph.setEndpoint(c, b, Endpoint.TAIL);
if (verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("R7: Single tails (tail)", graph.getEdge(c, b)));
}
// We know A--oBo-*C and A,C nonadjacent: R7 applies!
this.changeFlag = true;
}
}
}
}
/**
* Implements Zhang's rules R8, R9, R10, applies them over the graph once. Orient arrow tails. I.e., tries R8, R9,
* and R10 in that sequence on each Ao->C in the graph.
*/
public void rulesR8R9R10(Graph graph) {
List nodes = graph.getNodes();
for (Node c : nodes) {
if (Thread.currentThread().isInterrupted()) {
break;
}
List intoCArrows = graph.getNodesInTo(c, Endpoint.ARROW);
for (Node a : intoCArrows) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (!(graph.getEndpoint(c, a) == Endpoint.CIRCLE)) {
continue;
}
// We know Ao->C.
// Try each of R8, R9, R10 in that order, stopping ASAP.
if (!ruleR8(a, c, graph)) {
boolean b = ruleR9(a, c, graph);
if (!b) {
ruleR10(a, c, graph);
}
}
}
}
}
/**
* Orients the edges inside the definte discriminating path triangle. Takes the left endpoint, and a,b,c as
* arguments.
*/
public boolean doDdpOrientation(Node d, Node a, Node b, Node c, Graph graph) {
if (graph.isAdjacentTo(d, c)) {
throw new IllegalArgumentException();
}
Set sepset = getSepsets().getSepset(d, c);
if (this.verbose) {
logger.forceLogMessage("Sepset for d = " + d + " and c = " + c + " = " + sepset);
}
if (sepset == null) {
if (this.verbose) {
logger.forceLogMessage("Must be a sepset: " + d + " and " + c + "; they're non-adjacent.");
}
return false;
}
if (!sepset.contains(b) && doDiscriminatingPathColliderRule) {
if (!isArrowheadAllowed(a, b, graph, knowledge)) {
return false;
}
if (!isArrowheadAllowed(c, b, graph, knowledge)) {
return false;
}
graph.setEndpoint(a, b, Endpoint.ARROW);
graph.setEndpoint(c, b, Endpoint.ARROW);
if (this.verbose) {
this.logger.forceLogMessage(
"R4: Definite discriminating path collider rule d = " + d + " " + GraphUtils.pathString(graph, a, b, c));
}
this.changeFlag = true;
} else if (doDiscriminatingPathTailRule) {
graph.setEndpoint(c, b, Endpoint.TAIL);
if (this.verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg(
"R4: Definite discriminating path tail rule d = " + d, graph.getEdge(b, c)));
}
this.changeFlag = true;
return true;
}
return false;
}
/**
* Orients every edge on a path as undirected (i.e. A---B).
*
* DOES NOT CHECK IF SUCH EDGES ACTUALLY EXIST: MAY DO WEIRD THINGS IF PASSED AN ARBITRARY LIST OF NODES THAT IS NOT
* A PATH.
*
* @param path The path to orient as all tails.
*/
public void orientTailPath(List path, Graph graph) {
for (int i = 0; i < path.size() - 1; i++) {
Node n1 = path.get(i);
Node n2 = path.get(i + 1);
graph.setEndpoint(n1, n2, Endpoint.TAIL);
graph.setEndpoint(n2, n1, Endpoint.TAIL);
this.changeFlag = true;
if (verbose) {
this.logger.forceLogMessage("R8: Orient circle undirectedPaths " +
GraphUtils.pathString(graph, n1, n2));
}
}
}
/**
* Tries to apply Zhang's rule R8 to a pair of nodes A and C which are assumed to be such that Ao->C.
*
* MAY HAVE WEIRD EFFECTS ON ARBITRARY NODE PAIRS.
*
* R8: If Ao->C and A-->B-->C or A--oB-->C, then A-->C.
*
* @param a The node A.
* @param c The node C.
* @return Whether R8 was successfully applied.
*/
public boolean ruleR8(Node a, Node c, Graph graph) {
List intoCArrows = graph.getNodesInTo(c, Endpoint.ARROW);
for (Node b : intoCArrows) {
// We have B*->C.
if (!graph.isAdjacentTo(a, b)) {
continue;
}
if (!graph.isAdjacentTo(b, c)) {
continue;
}
// We have A*-*B*->C.
if (!(graph.getEndpoint(b, a) == Endpoint.TAIL)) {
continue;
}
if (!(graph.getEndpoint(c, b) == Endpoint.TAIL)) {
continue;
}
// We have A--*B-->C.
if (graph.getEndpoint(a, b) == Endpoint.TAIL) {
continue;
}
// We have A-->B-->C or A--oB-->C: R8 applies!
graph.setEndpoint(c, a, Endpoint.TAIL);
if (verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("R8: ", graph.getEdge(c, a)));
}
this.changeFlag = true;
return true;
}
return false;
}
/**
* Tries to apply Zhang's rule R9 to a pair of nodes A and C which are assumed to be such that Ao->C.
*
* MAY HAVE WEIRD EFFECTS ON ARBITRARY NODE PAIRS.
*
* R9: If Ao->C and there is an uncovered p.d. path u=<A,B,..,C> such that C,B nonadjacent, then A-->C.
*
* @param a The node A.
* @param c The node C.
* @return Whether R9 was succesfully applied.
*/
public boolean ruleR9(Node a, Node c, Graph graph) {
Edge e = graph.getEdge(a, c);
if (e == null) return false;
if (!e.equals(Edges.partiallyOrientedEdge(a, c))) return false;
List> ucPdPsToC = getUcPdPaths(a, c, graph);
for (List u : ucPdPsToC) {
Node b = u.get(1);
if (graph.isAdjacentTo(b, c)) {
continue;
}
if (b == c) {
continue;
}
// We know u is as required: R9 applies!
graph.setEndpoint(c, a, Endpoint.TAIL);
if (verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("R9: ", graph.getEdge(c, a)));
}
this.changeFlag = true;
return true;
}
return false;
}
/**
* Orients according to background knowledge
*/
public void fciOrientbk(Knowledge bk, Graph graph, List variables) {
this.logger.forceLogMessage("Starting BK Orientation.");
for (Iterator it
= bk.forbiddenEdgesIterator(); it.hasNext(); ) {
if (Thread.currentThread().isInterrupted()) {
break;
}
KnowledgeEdge edge = it.next();
//match strings to variables in the graph.
Node from = GraphSearchUtils.translate(edge.getFrom(), variables);
Node to = GraphSearchUtils.translate(edge.getTo(), variables);
if (from == null || to == null) {
continue;
}
if (graph.getEdge(from, to) == null) {
continue;
}
if (!isArrowheadAllowed(to, from, graph, knowledge)) {
return;
}
// Orient to*->from
graph.setEndpoint(to, from, Endpoint.ARROW);
this.changeFlag = true;
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("Knowledge", graph.getEdge(from, to)));
}
for (Iterator it
= bk.requiredEdgesIterator(); it.hasNext(); ) {
if (Thread.currentThread().isInterrupted()) {
break;
}
KnowledgeEdge edge = it.next();
//match strings to variables in this graph
Node from = GraphSearchUtils.translate(edge.getFrom(), variables);
Node to = GraphSearchUtils.translate(edge.getTo(), variables);
if (from == null || to == null) {
continue;
}
if (graph.getEdge(from, to) == null) {
continue;
}
if (!isArrowheadAllowed(from, to, graph, knowledge)) {
return;
}
graph.setEndpoint(to, from, Endpoint.TAIL);
graph.setEndpoint(from, to, Endpoint.ARROW);
this.changeFlag = true;
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("Knowledge", graph.getEdge(from, to)));
}
this.logger.forceLogMessage("Finishing BK Orientation.");
}
/**
* @return the maximum length of any discriminating path, or -1 of unlimited.
*/
public int getMaxPathLength() {
return this.maxPathLength;
}
/**
* @param maxPathLength the maximum length of any discriminating path, or -1 if unlimited.
*/
public void setMaxPathLength(int maxPathLength) {
if (maxPathLength < -1) {
throw new IllegalArgumentException("Max path length must be -1 (unlimited) or >= 0: " + maxPathLength);
}
this.maxPathLength = maxPathLength;
}
/**
* Sets whether verbose output is printed.
*
* @param verbose True, if so.
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* The true PAG if available. Can be null.
*/
public Graph getTruePag() {
return this.truePag;
}
/**
* Sets the true PAG for comparison.
*
* @param truePag This PAG.
*/
public void setTruePag(Graph truePag) {
this.truePag = truePag;
}
/**
* Change flag for repeat rules
*
* @return True if a change has occurred.
*/
public boolean isChangeFlag() {
return this.changeFlag;
}
/**
* Sets the change flag--marks externally that a change has been made.
*
* @param changeFlag This flag.
*/
public void setChangeFlag(boolean changeFlag) {
this.changeFlag = changeFlag;
}
/**
* Sets whether the discriminating path collider rule should be done.
*
* @param doDiscriminatingPathColliderRule True is done.
*/
public void setDoDiscriminatingPathColliderRule(boolean doDiscriminatingPathColliderRule) {
this.doDiscriminatingPathColliderRule = doDiscriminatingPathColliderRule;
}
/**
* Sets whether the discriminating path tail rule should be done.
*
* @param doDiscriminatingPathTailRule True if done.
*/
public void setDoDiscriminatingPathTailRule(boolean doDiscriminatingPathTailRule) {
this.doDiscriminatingPathTailRule = doDiscriminatingPathTailRule;
}
/**
* Tries to apply Zhang's rule R10 to a pair of nodes A and C which are assumed to be such that Ao->C.
*
* MAY HAVE WEIRD EFFECTS ON ARBITRARY NODE PAIRS.
*
* R10: If Ao->C, B-->C<--D, there is an uncovered p.d. path u1=<A,M,...,B> and an uncovered p.d. path u2=
* <A,N,...,D> with M != N and M,N nonadjacent then A-->C.
*
* @param a The node A.
* @param c The node C.
*/
public void ruleR10(Node a, Node c, Graph graph) {
List intoCArrows = graph.getNodesInTo(c, Endpoint.ARROW);
for (Node b : intoCArrows) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (b == a) {
continue;
}
if (!(graph.getEndpoint(c, b) == Endpoint.TAIL)) {
continue;
}
// We know Ao->C and B-->C.
for (Node d : intoCArrows) {
if (Thread.currentThread().isInterrupted()) {
break;
}
if (d == a || d == b) {
continue;
}
if (!(graph.getEndpoint(d, c) == Endpoint.TAIL)) {
continue;
}
// We know Ao->C and B-->C<--D.
List> ucPdPsToB = getUcPdPaths(a, b, graph);
List> ucPdPsToD = getUcPdPaths(a, d, graph);
for (List u1 : ucPdPsToB) {
if (Thread.currentThread().isInterrupted()) {
break;
}
Node m = u1.get(1);
for (List u2 : ucPdPsToD) {
if (Thread.currentThread().isInterrupted()) {
break;
}
Node n = u2.get(1);
if (m.equals(n)) {
continue;
}
if (graph.isAdjacentTo(m, n)) {
continue;
}
// We know B,D,u1,u2 as required: R10 applies!
graph.setEndpoint(c, a, Endpoint.TAIL);
if (verbose) {
this.logger.forceLogMessage(LogUtilsSearch.edgeOrientedMsg("R10: ", graph.getEdge(c, a)));
}
this.changeFlag = true;
return;
}
}
}
}
}
private void printWrongColliderMessage(Node a, Node b, Node c, Graph graph) {
if (this.truePag != null && graph.isDefCollider(a, b, c) && !this.truePag.isDefCollider(a, b, c)) {
logger.forceLogMessage("R0" + ": Orienting collider by mistake: " + a + "*->;" + b + "<-*" + c);
}
}
}