edu.cmu.tetrad.search.Fasd 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;
import edu.cmu.tetrad.data.Knowledge;
import edu.cmu.tetrad.graph.*;
import edu.cmu.tetrad.search.test.IndependenceResult;
import edu.cmu.tetrad.search.utils.LogUtilsSearch;
import edu.cmu.tetrad.search.utils.SepsetMap;
import edu.cmu.tetrad.util.ChoiceGenerator;
import edu.cmu.tetrad.util.NumberFormatUtil;
import edu.cmu.tetrad.util.TetradLogger;
import java.io.PrintStream;
import java.text.NumberFormat;
import java.util.*;
/**
* Adjusts FAS (see) for the deterministic case by refusing to removed edges based on conditional independence tests
* that are judged to be deterministic. That is, if X _||_ Y | Z, but Z determines X or Y, then the edge X---Y is not
* removed.
*
* This class is configured to respect knowledge of forbidden and required edges, including knowledge of temporal
* tiers.
*
* @author peterspirtes
* @author josephramsey.
* @version $Id: $Id
* @see Fas
* @see Knowledge
*/
public class Fasd implements IFas {
/**
* The independence test. This should be appropriate to the types
*/
private final IndependenceTest test;
/**
* The logger, by default the empty logger.
*/
private final TetradLogger logger = TetradLogger.getInstance();
/**
* The number formatter.
*/
private final NumberFormat nf = NumberFormatUtil.getInstance().getNumberFormat();
/**
* The search graph. It is assumed going in that all the true adjacencies of x are in this graph for every node x.
* It is hoped (i.e., true in the large sample limit) that true adjacencies are never removed.
*/
private final Graph graph;
/**
* Specification of which edges are forbidden or required.
*/
private Knowledge knowledge = new Knowledge();
/**
* The maximum number of variables conditioned on in any conditional independence test. If the depth is -1, it will
* be taken to be the maximum value, which is 1000. Otherwise, it should be set to a non-negative integer.
*/
private int depth = 1000;
/**
* The number of independence tests.
*/
private int numIndependenceTests;
/**
* The sepsets found during the search.
*/
private SepsetMap sepset = new SepsetMap();
/**
* The depth 0 graph, specified initially.
*/
private Graph externalGraph;
/**
* True iff verbose output should be printed.
*/
private boolean verbose;
/**
* The output stream.
*/
private transient PrintStream out = System.out;
/**
* Constructs a new FastAdjacencySearch.
*
* @param test A test to use as a conditional independence oracle.
*/
public Fasd(IndependenceTest test) {
this.graph = new EdgeListGraph(test.getVariables());
this.test = test;
}
/**
* Discovers all adjacencies in data. The procedure is to remove edges in the graph which connect pairs of
* variables which are independent, conditional on some other set of variables in the graph (the "sepset"). These
* are removed in tiers. First, edges which are independent conditional on zero other variables are removed, then
* edges which are independent conditional on one other variable are removed, then two, then three, and so on, until
* no more edges can be removed from the graph. The edges which remain in the graph after this procedure are the
* adjacencies in the data.
*
* @return a graph which indicates which variables are independent conditional on which other variables
*/
public Graph search() {
TetradLogger.getInstance().log("Starting Fast Adjacency Search.");
this.graph.removeEdges(this.graph.getEdges());
this.sepset = new SepsetMap();
int _depth = this.depth;
if (_depth == -1) {
_depth = 1000;
}
Map> adjacencies = new HashMap<>();
List nodes = this.graph.getNodes();
for (Node node : nodes) {
adjacencies.put(node, new TreeSet<>());
}
for (int d = 0; d <= _depth; d++) {
boolean more;
if (d == 0) {
more = searchAtDepth0(nodes, this.test, adjacencies);
} else {
more = searchAtDepth(nodes, this.test, adjacencies, d);
}
if (!more) {
break;
}
}
for (int i = 0; i < nodes.size(); i++) {
for (int j = i + 1; j < nodes.size(); j++) {
Node x = nodes.get(i);
Node y = nodes.get(j);
if (adjacencies.get(x).contains(y)) {
this.graph.addUndirectedEdge(x, y);
}
}
}
TetradLogger.getInstance().log("Finishing Fast Adjacency Search.");
return this.graph;
}
/**
* Sets the depth of the search.
*
* @param depth The maximum search depth. Must be -1 (unlimited) or >= 0.
* @throws IllegalArgumentException if depth is less than -1
*/
public void setDepth(int depth) {
if (depth < -1) {
throw new IllegalArgumentException(
"Depth must be -1 (unlimited) or >= 0.");
}
this.depth = depth;
}
/**
* Sets the knowledge for this object.
*
* @param knowledge The knowledge to set. Cannot be null.
* @throws NullPointerException If knowledge is null.
*/
public void setKnowledge(Knowledge knowledge) {
if (knowledge == null) {
throw new NullPointerException("Cannot set knowledge to null");
}
this.knowledge = knowledge;
}
/**
* Returns the number of conditional independence tests done in the course of search.
*
* @return This number.
*/
public int getNumIndependenceTests() {
return this.numIndependenceTests;
}
/**
* Returns the map of node pairs to sepsets from the search.
*
* @return This map.
*/
public SepsetMap getSepsets() {
return this.sepset;
}
/**
* Sets the external graph. Adjacencies not in this external graph will not be judged adjacent in the search
* result.
*
* @param externalGraph This graph.
*/
public void setExternalGraph(Graph externalGraph) {
this.externalGraph = externalGraph;
}
/**
* Returns the current value of the verbose flag.
*
* @return true if verbose output is enabled, false otherwise.
*/
public boolean isVerbose() {
return this.verbose;
}
/**
* Sets the verbose flag to control verbose output.
*
* @param verbose True, if verbose output is enabled. False otherwise.
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* Returns the elapsed time of the method execution.
*
* @return the elapsed time in milliseconds
*/
@Override
public long getElapsedTime() {
return 0;
}
/**
* Retrieves the list of nodes from the current object.
*
* @return A list of Node objects representing the nodes in this object.
*/
@Override
public List getNodes() {
return this.test.getVariables();
}
/**
* Retrieves a list of ambiguous triples for the given node.
*
* @param node The node for which to retrieve the ambiguous triples.
* @return A list of Triple objects representing the ambiguous triples.
*/
@Override
public List getAmbiguousTriples(Node node) {
return new ArrayList<>();
}
/**
* Sets the output stream for this object.
*
* @param out The output stream to be set.
*/
@Override
public void setOut(PrintStream out) {
this.out = out;
}
/**
* Searches for adjacencies at depth 0 in the given list of nodes using the provided independence test and
* adjacencies map.
*
* @param nodes The list of nodes.
* @param test The independence test to use.
* @param adjacencies The map of adjacencies.
* @return True if there are free degrees in the graph after the search, false otherwise.
*/
private boolean searchAtDepth0(List nodes, IndependenceTest test, Map> adjacencies) {
Set empty = Collections.emptySet();
for (int i = 0; i < nodes.size(); i++) {
if (this.verbose) {
if ((i + 1) % 100 == 0) this.out.println("Node # " + (i + 1));
}
Node x = nodes.get(i);
for (int j = i + 1; j < nodes.size(); j++) {
Node y = nodes.get(j);
if (this.externalGraph != null) {
Node x2 = this.externalGraph.getNode(x.getName());
Node y2 = this.externalGraph.getNode(y.getName());
if (!this.externalGraph.isAdjacentTo(x2, y2)) {
continue;
}
}
IndependenceResult result;
try {
this.numIndependenceTests++;
result = test.checkIndependence(x, y, empty);
} catch (Exception e) {
result = new IndependenceResult(new IndependenceFact(x, y, empty), false, Double.NaN, Double.NaN);
}
boolean noEdgeRequired =
this.knowledge.noEdgeRequired(x.getName(), y.getName());
if (result.isIndependent() && noEdgeRequired) {
getSepsets().set(x, y, empty);
String message = LogUtilsSearch.independenceFact(x, y, empty) + " p = " +
this.nf.format(result.getPValue());
TetradLogger.getInstance().log(message);
if (this.verbose) {
this.out.println(LogUtilsSearch.independenceFact(x, y, empty) + " p = " +
this.nf.format(result.getPValue()));
}
} else if (!forbiddenEdge(x, y)) {
adjacencies.get(x).add(y);
adjacencies.get(y).add(x);
String message = LogUtilsSearch.independenceFact(x, y, empty) + " p = " +
this.nf.format(result.getPValue());
TetradLogger.getInstance().log(message);
}
}
}
return
freeDegree(nodes, adjacencies)
> 0;
}
/**
* Calculates the free degree of a graph.
*
* @param nodes The list of nodes in the graph.
* @param adjacencies The map of adjacencies for each node.
* @return The maximum free degree in the graph.
*/
private int freeDegree(List nodes, Map> adjacencies) {
int max = 0;
for (Node x : nodes) {
Set opposites = adjacencies.get(x);
for (Node y : opposites) {
Set adjx = new HashSet<>(opposites);
adjx.remove(y);
if (adjx.size() > max) {
max = adjx.size();
}
}
}
return max;
}
/**
* Checks if a given edge between two nodes is forbidden based on background knowledge.
*
* @param x The first node.
* @param y The second node.
* @return True if the edge is forbidden, false otherwise.
*/
private boolean forbiddenEdge(Node x, Node y) {
String name1 = x.getName();
String name2 = y.getName();
if (this.knowledge.isForbidden(name1, name2) &&
this.knowledge.isForbidden(name2, name1)) {
String message = "Removed " + Edges.undirectedEdge(x, y) + " because it was " +
"forbidden by background knowledge.";
TetradLogger.getInstance().log(message);
return true;
}
return false;
}
/**
* Search for adjacencies at a given depth in the given list of nodes using the provided independence test and
* adjacencies map.
*
* @param nodes The list of nodes.
* @param test The independence test to use.
* @param adjacencies The map of adjacencies.
* @param depth The depth of the search.
* @return True if there are free degrees in the graph after the search, false otherwise.
*/
private boolean searchAtDepth(List nodes, IndependenceTest test, Map> adjacencies, int depth) {
int count = 0;
List facts = new ArrayList<>();
for (Node x : nodes) {
if (this.verbose) {
if (++count % 100 == 0) this.out.println("count " + count + " of " + nodes.size());
}
List adjx = new ArrayList<>(adjacencies.get(x));
EDGE:
for (Node y : adjx) {
List _adjx = new ArrayList<>(adjacencies.get(x));
_adjx.remove(y);
List ppx = possibleParents(x, _adjx, this.knowledge);
if (ppx.size() >= depth) {
ChoiceGenerator cg = new ChoiceGenerator(ppx.size(), depth);
int[] choice;
while ((choice = cg.next()) != null) {
Set condSet = GraphUtils.asSet(choice, ppx);
IndependenceFact fact = new IndependenceFact(x, y, condSet);
if (facts.contains(fact)) continue;
facts.add(fact);
boolean independent;
try {
this.numIndependenceTests++;
independent = test.checkIndependence(x, y, condSet).isIndependent();
} catch (Exception e) {
independent = false;
}
boolean noEdgeRequired =
this.knowledge.noEdgeRequired(x.getName(), y.getName());
if (independent && noEdgeRequired) {
adjacencies.get(x).remove(y);
adjacencies.get(y).remove(x);
getSepsets().set(x, y, condSet);
continue EDGE;
}
}
}
}
}
return freeDegree(nodes, adjacencies) > depth;
}
/**
* Returns a list of possible parent nodes for a given node based on the adjacency list and knowledge.
*
* @param x The node for which to find possible parent nodes.
* @param adjx The adjacency list of the given node.
* @param knowledge The knowledge object containing background knowledge.
* @return A list of possible parent nodes for the given node.
*/
private List possibleParents(Node x, List adjx,
Knowledge knowledge) {
List possibleParents = new LinkedList<>();
String _x = x.getName();
for (Node z : adjx) {
String _z = z.getName();
if (possibleParentOf(_z, _x, knowledge)) {
possibleParents.add(z);
}
}
return possibleParents;
}
/**
* Returns whether a given node 'z' is a possible parent node of node 'x' based on the adjacency list and background
* knowledge.
*
* @param z The node to check if it is a possible parent of 'x'.
* @param x The node for which to find possible parent nodes.
* @param knowledge The knowledge object containing background knowledge.
* @return true if 'z' is a possible parent of 'x', false otherwise.
*/
private boolean possibleParentOf(String z, String x, Knowledge knowledge) {
return !knowledge.isForbidden(z, x) && !knowledge.isRequired(x, z);
}
}