From 7ce1d59296ed63c60c716f9a1ba41a1f4651e62e Mon Sep 17 00:00:00 2001 From: Duncan Murdoch Date: Fri, 11 Dec 2020 19:49:24 -0500 Subject: [PATCH 01/17] Allow expressions as node labels --- .gitignore | 1 + r/R/dagitty.r | 15 +++++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 783cd5b..28678a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.swp +.Rproj.user diff --git a/r/R/dagitty.r b/r/R/dagitty.r index 9bd21ae..5928ad4 100644 --- a/r/R/dagitty.r +++ b/r/R/dagitty.r @@ -1081,6 +1081,7 @@ graphLayout <- function( x, method="spring" ){ plot.dagitty <- function( x, abbreviate.names=FALSE, show.coefficients=FALSE, + nodenames=NULL, ... ){ x <- as.dagitty( x ) .supportsTypes(x,c("dag","mag","pdag")) @@ -1095,13 +1096,17 @@ plot.dagitty <- function( x, } else { labels <- names(coords$x) } + if(!is.null(nodenames)){ + names(labels) <- names(coords$x) + labels[names(nodenames)] <- nodenames + } omar <- par("mar") par(mar=rep(0,4)) plot.new() par(new=TRUE) - wx <- sapply( paste0("mm",labels), + wx <- strwidth("mm",units="inches") + sapply( labels, function(s) strwidth(s,units="inches") ) - wy <- sapply( paste0("\n",labels), + wy <- strheight("\n") + sapply( labels, function(s) strheight(s,units="inches") ) ppi.x <- dev.size("in")[1] / (max(coords$x)-min(coords$x)) ppi.y <- dev.size("in")[2] / (max(coords$y)-min(coords$y)) @@ -1111,10 +1116,8 @@ plot.dagitty <- function( x, ylim <- c(-max(coords$y+wy/2),-min(coords$y-wy/2)) plot( NA, xlim=xlim, ylim=ylim, xlab="", ylab="", bty="n", xaxt="n", yaxt="n" ) - wx <- sapply( labels, - function(s) strwidth(paste0("xx",s)) ) - wy <- sapply( labels, - function(s) strheight(paste0("\n",s)) ) + wx <- strwidth("xx") + sapply( labels, strwidth ) + wy <- strheight("\n") + sapply( labels, strheight ) asp <- par("pin")[1]/diff(par("usr")[1:2]) / (par("pin")[2]/diff(par("usr")[3:4])) ex <- edges(x) From 88400cea4022f289b08b0a444e93bf7191722343 Mon Sep 17 00:00:00 2001 From: Duncan Murdoch Date: Fri, 11 Dec 2020 20:08:57 -0500 Subject: [PATCH 02/17] Document the changes --- r/R/dagitty.r | 12 ++++++++++-- r/man/plot.dagitty.Rd | 17 +++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/r/R/dagitty.r b/r/R/dagitty.r index 5928ad4..78093c5 100644 --- a/r/R/dagitty.r +++ b/r/R/dagitty.r @@ -1075,7 +1075,13 @@ graphLayout <- function( x, method="spring" ){ #' @param abbreviate.names logical. Whether to abbreviate variable names. #' @param show.coefficients logical. Whether to plot coefficients defined in the graph syntax #' on the edges. +#' @param nodenames If not NULL, a named vector or expression list +#' to rename the nodes. #' @param ... not used. +#' +#' @details If \code{nodenames} is not \code{NULL}, it should be a +#' named vector of characters or expressions to use to rename (some of) +#' the nodes, e.g. node \code{"X"} could be renamed using \code{expression(X = alpha^2)}. #' #' @export plot.dagitty <- function( x, @@ -1116,8 +1122,10 @@ plot.dagitty <- function( x, ylim <- c(-max(coords$y+wy/2),-min(coords$y-wy/2)) plot( NA, xlim=xlim, ylim=ylim, xlab="", ylab="", bty="n", xaxt="n", yaxt="n" ) - wx <- strwidth("xx") + sapply( labels, strwidth ) - wy <- strheight("\n") + sapply( labels, strheight ) + wx <- sapply( labels, strwidth ) + strwidth("xx") + names(wx) <- names(coords$x) + wy <- sapply( labels, strheight ) + strheight("\n") + names(wy) <- names(coords$x) asp <- par("pin")[1]/diff(par("usr")[1:2]) / (par("pin")[2]/diff(par("usr")[3:4])) ex <- edges(x) diff --git a/r/man/plot.dagitty.Rd b/r/man/plot.dagitty.Rd index f580011..a759301 100644 --- a/r/man/plot.dagitty.Rd +++ b/r/man/plot.dagitty.Rd @@ -4,8 +4,13 @@ \alias{plot.dagitty} \title{Plot Graph} \usage{ -\method{plot}{dagitty}(x, abbreviate.names = FALSE, - show.coefficients = FALSE, ...) +\method{plot}{dagitty}( + x, + abbreviate.names = FALSE, + show.coefficients = FALSE, + nodenames = NULL, + ... +) } \arguments{ \item{x}{the input graph, a DAG, MAG, or PDAG.} @@ -15,6 +20,9 @@ \item{show.coefficients}{logical. Whether to plot coefficients defined in the graph syntax on the edges.} +\item{nodenames}{If not NULL, a named vector or expression list +to rename the nodes.} + \item{...}{not used.} } \description{ @@ -22,3 +30,8 @@ A simple plot method to quickly visualize a graph. This is intended mainly for simple visualization purposes and not as a full-fledged graph drawing function. } +\details{ +If \code{nodenames} is not \code{NULL}, it should be a + named vector of characters or expressions to use to rename (some of) + the nodes, e.g. node \code{"X"} could be renamed using \code{expression(X = alpha^2)}. +} From 32312735ee4f939172abf79e81d102815779abe1 Mon Sep 17 00:00:00 2001 From: Duncan Murdoch Date: Sat, 12 Dec 2020 11:03:13 -0500 Subject: [PATCH 03/17] Cleanup and error checks --- r/R/dagitty.r | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/r/R/dagitty.r b/r/R/dagitty.r index 78093c5..55f183a 100644 --- a/r/R/dagitty.r +++ b/r/R/dagitty.r @@ -1104,16 +1104,23 @@ plot.dagitty <- function( x, } if(!is.null(nodenames)){ names(labels) <- names(coords$x) + if(is.null(names(nodenames))) + stop("'nodenames' must be named") + if(!all(names(nodenames) %in% names(labels))) + stop("node(s) not found: ", + paste0("'", setdiff(names(nodenames), names(labels)), "'", + collapse = ",")) labels[names(nodenames)] <- nodenames + names(labels) <- names(coords$x) # If coerced to expression, names are lost } omar <- par("mar") par(mar=rep(0,4)) plot.new() par(new=TRUE) - wx <- strwidth("mm",units="inches") + sapply( labels, - function(s) strwidth(s,units="inches") ) - wy <- strheight("\n") + sapply( labels, - function(s) strheight(s,units="inches") ) + wx <- sapply( labels, + function(s) strwidth(s,units="inches") ) + strwidth("mm",units="inches") + wy <- sapply( labels, + function(s) strheight(s,units="inches") ) + strheight("\n") ppi.x <- dev.size("in")[1] / (max(coords$x)-min(coords$x)) ppi.y <- dev.size("in")[2] / (max(coords$y)-min(coords$y)) wx <- wx/ppi.x @@ -1123,9 +1130,7 @@ plot.dagitty <- function( x, plot( NA, xlim=xlim, ylim=ylim, xlab="", ylab="", bty="n", xaxt="n", yaxt="n" ) wx <- sapply( labels, strwidth ) + strwidth("xx") - names(wx) <- names(coords$x) wy <- sapply( labels, strheight ) + strheight("\n") - names(wy) <- names(coords$x) asp <- par("pin")[1]/diff(par("usr")[1:2]) / (par("pin")[2]/diff(par("usr")[3:4])) ex <- edges(x) From 01de4616a6b3119fc8a3c829bb3dce9efa92bafa Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Sun, 23 Jan 2022 14:50:59 +0100 Subject: [PATCH 04/17] fixed bug with reserved words in connectedComponentAvoiding --- gui/js/dagitty.js | 10 ++++------ jslib/graph/GraphAnalyzer.js | 8 +++----- r/inst/js/dagitty-alg.js | 10 ++++------ test/tests.js | 10 +++++++++- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/gui/js/dagitty.js b/gui/js/dagitty.js index cfacdb1..9e49991 100644 --- a/gui/js/dagitty.js +++ b/gui/js/dagitty.js @@ -1513,9 +1513,7 @@ var GraphAnalyzer = { var g2 = g.clone() var vv = g2.vertices.values() // this ignores adjusted vertices for now - for( var i = 0 ; i < vv.length ; i ++ ){ - g2.removeAllAdjustedNodes() - } + g2.removeAllAdjustedNodes() var n = 0 for( i = 0 ; i < vv.length ; i ++ ){ for( var j = i+1 ; j < vv.length ; j ++ ){ @@ -1691,7 +1689,7 @@ var GraphAnalyzer = { } gr = g.clone(false) - visited = [] + visited = {} var followEdges = function( u, v, kin, edgetype, reverse ){ var st = [] @@ -2234,7 +2232,7 @@ var GraphAnalyzer = { * a subset of vertices */ connectedComponentAvoiding : function( g, V, U ){ - var visited = [], q = [], r = [] + var visited = {}, q = [], r = [] if( U instanceof Array ){ _.each( U, function(u){ visited[u.id]=1 }) } @@ -3157,7 +3155,7 @@ var GraphParser = { if( isdot && isdot.length > 1 ){ return this.parseDot( firstarg ) } else { - var hasarrow = firstarg.match( /(->|<->|<-|--)/mi ) + var hasarrow = firstarg.match( /(->|<->|<-)/mi ) // allow users to omit explicit "dag{ ... }" if at least one arrow is also specified if( hasarrow && hasarrow.length >= 1 ){ return this.parseDot( "dag{"+firstarg+"}" ) diff --git a/jslib/graph/GraphAnalyzer.js b/jslib/graph/GraphAnalyzer.js index d42f3c3..4c8697c 100644 --- a/jslib/graph/GraphAnalyzer.js +++ b/jslib/graph/GraphAnalyzer.js @@ -528,9 +528,7 @@ var GraphAnalyzer = { var g2 = g.clone() var vv = g2.vertices.values() // this ignores adjusted vertices for now - for( var i = 0 ; i < vv.length ; i ++ ){ - g2.removeAllAdjustedNodes() - } + g2.removeAllAdjustedNodes() var n = 0 for( i = 0 ; i < vv.length ; i ++ ){ for( var j = i+1 ; j < vv.length ; j ++ ){ @@ -706,7 +704,7 @@ var GraphAnalyzer = { } gr = g.clone(false) - visited = [] + visited = {} var followEdges = function( u, v, kin, edgetype, reverse ){ var st = [] @@ -1249,7 +1247,7 @@ var GraphAnalyzer = { * a subset of vertices */ connectedComponentAvoiding : function( g, V, U ){ - var visited = [], q = [], r = [] + var visited = {}, q = [], r = [] if( U instanceof Array ){ _.each( U, function(u){ visited[u.id]=1 }) } diff --git a/r/inst/js/dagitty-alg.js b/r/inst/js/dagitty-alg.js index 1cac51b..44db4cd 100644 --- a/r/inst/js/dagitty-alg.js +++ b/r/inst/js/dagitty-alg.js @@ -1508,9 +1508,7 @@ var GraphAnalyzer = { var g2 = g.clone() var vv = g2.vertices.values() // this ignores adjusted vertices for now - for( var i = 0 ; i < vv.length ; i ++ ){ - g2.removeAllAdjustedNodes() - } + g2.removeAllAdjustedNodes() var n = 0 for( i = 0 ; i < vv.length ; i ++ ){ for( var j = i+1 ; j < vv.length ; j ++ ){ @@ -1686,7 +1684,7 @@ var GraphAnalyzer = { } gr = g.clone(false) - visited = [] + visited = {} var followEdges = function( u, v, kin, edgetype, reverse ){ var st = [] @@ -2229,7 +2227,7 @@ var GraphAnalyzer = { * a subset of vertices */ connectedComponentAvoiding : function( g, V, U ){ - var visited = [], q = [], r = [] + var visited = {}, q = [], r = [] if( U instanceof Array ){ _.each( U, function(u){ visited[u.id]=1 }) } @@ -3152,7 +3150,7 @@ var GraphParser = { if( isdot && isdot.length > 1 ){ return this.parseDot( firstarg ) } else { - var hasarrow = firstarg.match( /(->|<->|<-|--)/mi ) + var hasarrow = firstarg.match( /(->|<->|<-)/mi ) // allow users to omit explicit "dag{ ... }" if at least one arrow is also specified if( hasarrow && hasarrow.length >= 1 ){ return this.parseDot( "dag{"+firstarg+"}" ) diff --git a/test/tests.js b/test/tests.js index be9e690..0301c9e 100644 --- a/test/tests.js +++ b/test/tests.js @@ -6,7 +6,11 @@ var $es = function(g){ return GraphSerializer.toDotEdgeStatements(g) } QUnit.test( "graph manipulation", function( assert ) { var g = $p( "dag G { x <-> x }" ) - assert.equal( g.areAdjacent("x","x"), true ) + assert.equal( g.areAdjacent("x","x"), true, "self loop" ) + + g = $p( "map <-> pop" ) + g.changeEdge( g.getEdge("map","pop",Graph.Edgetype.Bidirected), Graph.Edgetype.Directed ) + assert.equal( $es( g ), "map -> pop", "reserved words" ) g = $p( "dag G { x <-> y }" ) assert.equal( g.areAdjacent("x","y"), true ) @@ -280,6 +284,10 @@ QUnit.test( "separators", function( assert ) { function verts(a) { return _.isArray(a) ? a.map(function(vid){return g.getVertex(vid)}) : [g.getVertex(a)] } + + + g = $p("length -> push -> pop map -> { length push }") + assert.equal( GraphAnalyzer.listMinimalImplications( g ).length, 2, "reserverd words" ) g = GraphTransformer.backDoorGraph(TestGraphs.small1()) assert.equal( sep_2_str( GraphAnalyzer.listMinimalSeparators(g) ), "{}" ) From 19c73d8b15cf178a2a1855b1e3723281185d48b9 Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Sun, 23 Jan 2022 15:06:03 +0100 Subject: [PATCH 05/17] allow empty argument in children, parents etc --- r/R/internal.r | 12 ++++++++++-- r/tests/testthat/testBasics.R | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/r/R/internal.r b/r/R/internal.r index 88329ff..2e263d6 100644 --- a/r/R/internal.r +++ b/r/R/internal.r @@ -277,8 +277,16 @@ } .checkName <- function(x, v) { - if (!(v %in% names(x))) - stop(paste(v, "is not a variable in `x`")) + # allow "no" names for convenience + if( length(v) == 0 ){ + return() + } + if( length(v) > 1 ){ + stop("This function expects a variable name") + } + if (!(v %in% names(x))){ + stop(paste(v, "is not a variable in `x`")) + } } .checkAllNames <- function(x, vv) { diff --git a/r/tests/testthat/testBasics.R b/r/tests/testthat/testBasics.R index 104c907..d98e82e 100644 --- a/r/tests/testthat/testBasics.R +++ b/r/tests/testthat/testBasics.R @@ -79,3 +79,10 @@ test_that("equiv class", { expect_equal( length(equivalentDAGs("dag{a->{b c d} b->{c d}}")), 10 ) expect_equal( length(equivalentDAGs("dag{a->{b c d} b->{c d}}",3)), 3 ) } ) + + +test_that("children and parents of nothing", { + expect_equal( length(children("a->b",c())), 0 ) + expect_equal( length(parents("a->b",c())), 0 ) +} ) + From 58e7b79c45aab051c18da6126720674da95506f5 Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Sun, 23 Jan 2022 17:39:01 +0100 Subject: [PATCH 06/17] initial test of selection diagrams --- gui/dags.html | 7 ++++++ gui/js/dagitty.js | 44 ++++++++++++++++++++++++++++++--- gui/js/example-dags.js | 26 ++++++++++++++++++- gui/js/main.js | 12 +++++++++ gui/js/styles/original.js | 1 + jslib/graph/GraphTransformer.js | 23 +++++++++++++++++ jslib/graph/ObservedGraph.js | 4 ++- jslib/gui/GraphGUI_View.js | 17 ++++++++++--- r/inst/js/dagitty-alg.js | 27 +++++++++++++++++++- 9 files changed, 151 insertions(+), 10 deletions(-) diff --git a/gui/dags.html b/gui/dags.html index f04c5df..5697977 100644 --- a/gui/dags.html +++ b/gui/dags.html @@ -178,6 +178,7 @@
  • Set exposure variable
  • Set outcome variable
  • Adjust for variable
  • +
  • Define a selection variable
  • Make a variable unobserved (latent)
  • @@ -233,6 +234,9 @@

    +

    +

    @@ -259,6 +263,9 @@

    +

    +

    diff --git a/gui/js/dagitty.js b/gui/js/dagitty.js index 9e49991..7e5f501 100644 --- a/gui/js/dagitty.js +++ b/gui/js/dagitty.js @@ -3421,6 +3421,29 @@ var GraphTransformer = { _.intersection(g.ancestorsOf( Y, clearVisitedWhereNotAdjusted ), g.descendantsOf( X, clearVisitedWhereNotAdjusted ) ) ) }, + + /** Retain subgraph that contains the paths linking X to S conditioned on Y. + * Take nodes into account that have already been adjusted for. + */ + activeSelectionBiasGraph : function( g, x, y, s ){ + var r = new Graph() + _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) + g = g.clone() + g.removeSelectedNode( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + g.addAdjustedNode( y ) + g.removeTarget( y ) + g.addTarget( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + _.each( this.causalFlowGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + return r + }, /** * This function returns the subgraph of this graph that is induced @@ -4452,7 +4475,9 @@ var ObservedGraph = Class.extend({ "addLatentNode" : "change", "removeLatentNode" : "change", "addAdjustedNode" : "change", - "removeAdjustedNode" : "change" + "removeAdjustedNode" : "change", + "addSelectedNode" : "change", + "removeSelectedNode" : "change" }, observe : function( event, listener ){ @@ -6574,7 +6599,6 @@ var DAGittyGraphView = Class.extend({ clickHandler : function(e){ // click handler can be set to emulate keypress action // using this function - console.log(" click handler called on canvas" ) this.last_click_x = this.pointerX(e)-this.getContainer().offsetLeft this.last_click_y = this.pointerY(e)-this.getContainer().offsetTop this.last_click_g_coords = this.toGraphCoordinate( this.last_click_x, this.last_click_y ) @@ -6626,6 +6650,9 @@ var DAGittyGraphView = Class.extend({ e.preventDefault() } break + case 83: //s + if(v) this.toggleVertexProperty(v,"selectedNode") + break case 85: //u if(v) this.toggleVertexProperty(v,"latentNode") break @@ -6903,6 +6930,7 @@ var DAGittyGraphView = Class.extend({ var g,i,c var g_causal = new Graph() var g_bias = new Graph() + var g_selection_bias = new Graph() var g_trr = new Graph() var g_an = GraphTransformer.ancestorGraph( @@ -6923,6 +6951,13 @@ var DAGittyGraphView = Class.extend({ case "equivalence" : g = GraphTransformer.dagToCpdag( this.getGraph() ) break + case "causalodds": + g = this.getGraph() + if( g.getSources().length == 1 && g.getTargets().length == 1 && g.getSelectedNodes().length == 1 ){ + g_causal = GraphTransformer.causalFlowGraph(g) + g_bias = GraphTransformer.activeSelectionBiasGraph( g, g.getSources()[0], g.getTargets()[0], g.getSelectedNodes()[0] ) + } + break default: g = this.getGraph() g_trr = GraphTransformer.transitiveReduction( g ) @@ -6961,6 +6996,8 @@ var DAGittyGraphView = Class.extend({ vertex_type = "outcome" } else if( g.isAdjustedNode(vv[i]) ){ vertex_type = "adjusted" + } else if( g.isSelectedNode(vv[i]) ){ + vertex_type = "selected" } else if( g.isLatentNode(vv[i]) ){ vertex_type = "latent" } else if( ean_ids[vv[i].id] @@ -6970,8 +7007,7 @@ var DAGittyGraphView = Class.extend({ vertex_type = "anexposure" } else if( oan_ids[vv[i].id] ){ vertex_type = "anoutcome" - } - + } this.impl.createVertexShape( vertex_type, vs ) this.vertex_shapes.set( vv[i].id, vs ) } diff --git a/gui/js/example-dags.js b/gui/js/example-dags.js index 5bc3ce1..b4f87fb 100644 --- a/gui/js/example-dags.js +++ b/gui/js/example-dags.js @@ -342,6 +342,30 @@ e:"e0 x\n"+ "PER DET\n", l: "van Kampen, 2014" -} +}, +{ + "d": ` +dag { +Age [adjusted,pos="-1.973,-0.123"] +HRT [exposure,pos="-0.536,-0.016"] +Occ [pos="-1.645,0.432"] +S [selected,pos="0.925,-0.500"] +Smo [pos="-0.879,0.441"] +TCI [outcome,pos="0.488,0.090"] +Thist [pos="-0.569,1.027"] +Age -> HRT +Age -> Occ +Age -> S [pos="-0.945,-0.571"] +Age -> TCI [pos="-0.264,-0.456"] +HRT -> TCI +Occ -> Smo +Occ -> Thist +Smo -> HRT +Smo -> TCI +TCI -> S +Thist -> TCI +}`, + l: "Didelez et al, 2010" + } ]; diff --git a/gui/js/main.js b/gui/js/main.js index 952690a..5a2e701 100644 --- a/gui/js/main.js +++ b/gui/js/main.js @@ -75,6 +75,7 @@ var GUI = { document.getElementById("variable_label").innerText = vid document.getElementById("variable_exposure").checked = Model.dag.isSource(vid) document.getElementById("variable_outcome").checked = Model.dag.isTarget(vid) + document.getElementById("variable_selected").checked = Model.dag.isSelectedNode(vid) document.getElementById("variable_adjusted").checked = Model.dag.isAdjustedNode(vid) document.getElementById("variable_unobserved").checked = Model.dag.isLatentNode( vid ) }, @@ -212,6 +213,12 @@ function setsToHTML( sets ){ } function causalEffectEstimates(){ + if( Model.dag.getSelectedNodes().length > 0 ){ + displayCausalMsg("I cannot determine causal effects for DAGs with selection nodes."); return + } + if( GraphAnalyzer.containsCycle( Model.dag ) ){ + displayCausalMsg("I cannot determine causal effects for cyclic models."); return + } switch( document.getElementById("causal_effect_kind").value ){ case "adj_total" : displayAdjustmentInfo("total"); break @@ -248,6 +255,11 @@ function msasToHtml( msas ){ } } +function displayCausalMsg( wh ){ + document.getElementById("causal_effect").innerHTML + = "

    "+wh+"

    "; +} + function displayAdjustmentInfo( kind ){ var adjusted_nodes = Model.dag.getAdjustedNodes(); var html_adjustment = ""; diff --git a/gui/js/styles/original.js b/gui/js/styles/original.js index 8d4c3a4..8b4c0ab 100644 --- a/gui/js/styles/original.js +++ b/gui/js/styles/original.js @@ -9,6 +9,7 @@ DAGitty.stylesheets.original = { exposurenode : { fill : "#bed403", stroke : "#000000" }, outcomenode : { fill : "#00a2e0", stroke : "#000000" }, adjustednode : { fill : "#ffffff", stroke : "#000000" }, + selectednode : { 'd' : 'M [rx], 0 L [rx],[ry] L -[rx],[ry] L -[rx],-[ry] L [rx],-[ry] Z' }, latentnode : { fill : "#dddddd", stroke: "#aaaaaa" /*"stroke-dasharray" : "5,5" */}, confoundernode : { fill : "#ff7777", stroke : "#ff7777" }, anexposurenode : { fill : "#bed403", stroke : "#bed403" }, diff --git a/jslib/graph/GraphTransformer.js b/jslib/graph/GraphTransformer.js index de2e2de..3aa4934 100644 --- a/jslib/graph/GraphTransformer.js +++ b/jslib/graph/GraphTransformer.js @@ -253,6 +253,29 @@ var GraphTransformer = { _.intersection(g.ancestorsOf( Y, clearVisitedWhereNotAdjusted ), g.descendantsOf( X, clearVisitedWhereNotAdjusted ) ) ) }, + + /** Retain subgraph that contains the paths linking X to S conditioned on Y. + * Take nodes into account that have already been adjusted for. + */ + activeSelectionBiasGraph : function( g, x, y, s ){ + var r = new Graph() + _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) + g = g.clone() + g.removeSelectedNode( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + g.addAdjustedNode( y ) + g.removeTarget( y ) + g.addTarget( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + _.each( this.causalFlowGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + return r + }, /** * This function returns the subgraph of this graph that is induced diff --git a/jslib/graph/ObservedGraph.js b/jslib/graph/ObservedGraph.js index 1b03beb..9b8d853 100644 --- a/jslib/graph/ObservedGraph.js +++ b/jslib/graph/ObservedGraph.js @@ -68,7 +68,9 @@ var ObservedGraph = Class.extend({ "addLatentNode" : "change", "removeLatentNode" : "change", "addAdjustedNode" : "change", - "removeAdjustedNode" : "change" + "removeAdjustedNode" : "change", + "addSelectedNode" : "change", + "removeSelectedNode" : "change" }, observe : function( event, listener ){ diff --git a/jslib/gui/GraphGUI_View.js b/jslib/gui/GraphGUI_View.js index 7068632..612c388 100644 --- a/jslib/gui/GraphGUI_View.js +++ b/jslib/gui/GraphGUI_View.js @@ -179,7 +179,6 @@ var DAGittyGraphView = Class.extend({ clickHandler : function(e){ // click handler can be set to emulate keypress action // using this function - console.log(" click handler called on canvas" ) this.last_click_x = this.pointerX(e)-this.getContainer().offsetLeft this.last_click_y = this.pointerY(e)-this.getContainer().offsetTop this.last_click_g_coords = this.toGraphCoordinate( this.last_click_x, this.last_click_y ) @@ -231,6 +230,9 @@ var DAGittyGraphView = Class.extend({ e.preventDefault() } break + case 83: //s + if(v) this.toggleVertexProperty(v,"selectedNode") + break case 85: //u if(v) this.toggleVertexProperty(v,"latentNode") break @@ -508,6 +510,7 @@ var DAGittyGraphView = Class.extend({ var g,i,c var g_causal = new Graph() var g_bias = new Graph() + var g_selection_bias = new Graph() var g_trr = new Graph() var g_an = GraphTransformer.ancestorGraph( @@ -528,6 +531,13 @@ var DAGittyGraphView = Class.extend({ case "equivalence" : g = GraphTransformer.dagToCpdag( this.getGraph() ) break + case "causalodds": + g = this.getGraph() + if( g.getSources().length == 1 && g.getTargets().length == 1 && g.getSelectedNodes().length == 1 ){ + g_causal = GraphTransformer.causalFlowGraph(g) + g_bias = GraphTransformer.activeSelectionBiasGraph( g, g.getSources()[0], g.getTargets()[0], g.getSelectedNodes()[0] ) + } + break default: g = this.getGraph() g_trr = GraphTransformer.transitiveReduction( g ) @@ -566,6 +576,8 @@ var DAGittyGraphView = Class.extend({ vertex_type = "outcome" } else if( g.isAdjustedNode(vv[i]) ){ vertex_type = "adjusted" + } else if( g.isSelectedNode(vv[i]) ){ + vertex_type = "selected" } else if( g.isLatentNode(vv[i]) ){ vertex_type = "latent" } else if( ean_ids[vv[i].id] @@ -575,8 +587,7 @@ var DAGittyGraphView = Class.extend({ vertex_type = "anexposure" } else if( oan_ids[vv[i].id] ){ vertex_type = "anoutcome" - } - + } this.impl.createVertexShape( vertex_type, vs ) this.vertex_shapes.set( vv[i].id, vs ) } diff --git a/r/inst/js/dagitty-alg.js b/r/inst/js/dagitty-alg.js index 44db4cd..e1f0cef 100644 --- a/r/inst/js/dagitty-alg.js +++ b/r/inst/js/dagitty-alg.js @@ -3416,6 +3416,29 @@ var GraphTransformer = { _.intersection(g.ancestorsOf( Y, clearVisitedWhereNotAdjusted ), g.descendantsOf( X, clearVisitedWhereNotAdjusted ) ) ) }, + + /** Retain subgraph that contains the paths linking X to S conditioned on Y. + * Take nodes into account that have already been adjusted for. + */ + activeSelectionBiasGraph : function( g, x, y, s ){ + var r = new Graph() + _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) + g = g.clone() + g.removeSelectedNode( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + g.addAdjustedNode( y ) + g.removeTarget( y ) + g.addTarget( s ) + _.each( this.activeBiasGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + _.each( this.causalFlowGraph( g ).getEdges(), function(e) { + r.addEdge( e.v1.id, e.v2.id, e.directed ) + } ) + return r + }, /** * This function returns the subgraph of this graph that is induced @@ -4447,7 +4470,9 @@ var ObservedGraph = Class.extend({ "addLatentNode" : "change", "removeLatentNode" : "change", "addAdjustedNode" : "change", - "removeAdjustedNode" : "change" + "removeAdjustedNode" : "change", + "addSelectedNode" : "change", + "removeSelectedNode" : "change" }, observe : function( event, listener ){ From 2b279cfb0779151bc5b249591acff76622478dfd Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Sun, 23 Jan 2022 18:20:46 +0100 Subject: [PATCH 07/17] multiple selection nodes --- gui/js/dagitty.js | 12 ++++++------ gui/js/styles/original.js | 2 +- jslib/graph/GraphTransformer.js | 6 +++--- jslib/gui/GraphGUI_View.js | 6 +++--- r/inst/js/dagitty-alg.js | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/gui/js/dagitty.js b/gui/js/dagitty.js index 7e5f501..23acb2b 100644 --- a/gui/js/dagitty.js +++ b/gui/js/dagitty.js @@ -3425,17 +3425,17 @@ var GraphTransformer = { /** Retain subgraph that contains the paths linking X to S conditioned on Y. * Take nodes into account that have already been adjusted for. */ - activeSelectionBiasGraph : function( g, x, y, s ){ + activeSelectionBiasGraph : function( g, x, y, S ){ var r = new Graph() _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) g = g.clone() - g.removeSelectedNode( s ) + _.each( S, function(s){ g.removeSelectedNode( s ) } ) _.each( this.activeBiasGraph( g ).getEdges(), function(e) { r.addEdge( e.v1.id, e.v2.id, e.directed ) } ) g.addAdjustedNode( y ) g.removeTarget( y ) - g.addTarget( s ) + _.each( S, function(s){ g.addTarget( s ) } ) _.each( this.activeBiasGraph( g ).getEdges(), function(e) { r.addEdge( e.v1.id, e.v2.id, e.directed ) } ) @@ -6953,9 +6953,9 @@ var DAGittyGraphView = Class.extend({ break case "causalodds": g = this.getGraph() - if( g.getSources().length == 1 && g.getTargets().length == 1 && g.getSelectedNodes().length == 1 ){ - g_causal = GraphTransformer.causalFlowGraph(g) - g_bias = GraphTransformer.activeSelectionBiasGraph( g, g.getSources()[0], g.getTargets()[0], g.getSelectedNodes()[0] ) + g_causal = GraphTransformer.causalFlowGraph(g) + if( g.getSources().length == 1 && g.getTargets().length == 1 && g.getSelectedNodes().length > 0 ){ + g_bias = GraphTransformer.activeSelectionBiasGraph( g, g.getSources()[0], g.getTargets()[0], g.getSelectedNodes() ) } break default: diff --git a/gui/js/styles/original.js b/gui/js/styles/original.js index 8b4c0ab..665dd7d 100644 --- a/gui/js/styles/original.js +++ b/gui/js/styles/original.js @@ -9,7 +9,7 @@ DAGitty.stylesheets.original = { exposurenode : { fill : "#bed403", stroke : "#000000" }, outcomenode : { fill : "#00a2e0", stroke : "#000000" }, adjustednode : { fill : "#ffffff", stroke : "#000000" }, - selectednode : { 'd' : 'M [rx], 0 L [rx],[ry] L -[rx],[ry] L -[rx],-[ry] L [rx],-[ry] Z' }, + selectednode : { 'd' : 'M 20, 0 L 20,15 L -20,15 L -20,-15 L 20,-15 Z' }, latentnode : { fill : "#dddddd", stroke: "#aaaaaa" /*"stroke-dasharray" : "5,5" */}, confoundernode : { fill : "#ff7777", stroke : "#ff7777" }, anexposurenode : { fill : "#bed403", stroke : "#bed403" }, diff --git a/jslib/graph/GraphTransformer.js b/jslib/graph/GraphTransformer.js index 3aa4934..ede7c17 100644 --- a/jslib/graph/GraphTransformer.js +++ b/jslib/graph/GraphTransformer.js @@ -257,17 +257,17 @@ var GraphTransformer = { /** Retain subgraph that contains the paths linking X to S conditioned on Y. * Take nodes into account that have already been adjusted for. */ - activeSelectionBiasGraph : function( g, x, y, s ){ + activeSelectionBiasGraph : function( g, x, y, S ){ var r = new Graph() _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) g = g.clone() - g.removeSelectedNode( s ) + _.each( S, function(s){ g.removeSelectedNode( s ) } ) _.each( this.activeBiasGraph( g ).getEdges(), function(e) { r.addEdge( e.v1.id, e.v2.id, e.directed ) } ) g.addAdjustedNode( y ) g.removeTarget( y ) - g.addTarget( s ) + _.each( S, function(s){ g.addTarget( s ) } ) _.each( this.activeBiasGraph( g ).getEdges(), function(e) { r.addEdge( e.v1.id, e.v2.id, e.directed ) } ) diff --git a/jslib/gui/GraphGUI_View.js b/jslib/gui/GraphGUI_View.js index 612c388..18f45c9 100644 --- a/jslib/gui/GraphGUI_View.js +++ b/jslib/gui/GraphGUI_View.js @@ -533,9 +533,9 @@ var DAGittyGraphView = Class.extend({ break case "causalodds": g = this.getGraph() - if( g.getSources().length == 1 && g.getTargets().length == 1 && g.getSelectedNodes().length == 1 ){ - g_causal = GraphTransformer.causalFlowGraph(g) - g_bias = GraphTransformer.activeSelectionBiasGraph( g, g.getSources()[0], g.getTargets()[0], g.getSelectedNodes()[0] ) + g_causal = GraphTransformer.causalFlowGraph(g) + if( g.getSources().length == 1 && g.getTargets().length == 1 && g.getSelectedNodes().length > 0 ){ + g_bias = GraphTransformer.activeSelectionBiasGraph( g, g.getSources()[0], g.getTargets()[0], g.getSelectedNodes() ) } break default: diff --git a/r/inst/js/dagitty-alg.js b/r/inst/js/dagitty-alg.js index e1f0cef..5252e8d 100644 --- a/r/inst/js/dagitty-alg.js +++ b/r/inst/js/dagitty-alg.js @@ -3420,17 +3420,17 @@ var GraphTransformer = { /** Retain subgraph that contains the paths linking X to S conditioned on Y. * Take nodes into account that have already been adjusted for. */ - activeSelectionBiasGraph : function( g, x, y, s ){ + activeSelectionBiasGraph : function( g, x, y, S ){ var r = new Graph() _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) g = g.clone() - g.removeSelectedNode( s ) + _.each( S, function(s){ g.removeSelectedNode( s ) } ) _.each( this.activeBiasGraph( g ).getEdges(), function(e) { r.addEdge( e.v1.id, e.v2.id, e.directed ) } ) g.addAdjustedNode( y ) g.removeTarget( y ) - g.addTarget( s ) + _.each( S, function(s){ g.addTarget( s ) } ) _.each( this.activeBiasGraph( g ).getEdges(), function(e) { r.addEdge( e.v1.id, e.v2.id, e.directed ) } ) From 465782d023a1b405803ddb26d4d8bf815912f048 Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Tue, 1 Mar 2022 10:23:33 -0800 Subject: [PATCH 08/17] comment out TreeID for the time being --- gui/dags.html | 6 +++--- gui/js/dagitty.js | 2 +- gui/js/main.js | 6 ++++-- jslib/graph/GraphAnalyzer.js | 2 +- r/inst/js/dagitty-alg.js | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/gui/dags.html b/gui/dags.html index 685f74c..369b653 100644 --- a/gui/dags.html +++ b/gui/dags.html @@ -4,7 +4,7 @@ - DAGitty v3.0 + DAGitty v3.1 @@ -377,7 +377,7 @@

    - +

    diff --git a/gui/js/dagitty.js b/gui/js/dagitty.js index 087941f..5f79145 100644 --- a/gui/js/dagitty.js +++ b/gui/js/dagitty.js @@ -3111,7 +3111,7 @@ var GraphAnalyzer = { } var res = {"results": IDobject} - if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. The algorithm assumes all nodes except a certain root node have exactly one parent, and ignores nodes with more parents"] + if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. TreeID assumes all nodes except one (the root) have exactly one parent, and ignores nodes with more parents."] return res } } diff --git a/gui/js/main.js b/gui/js/main.js index d60912d..317eea6 100644 --- a/gui/js/main.js +++ b/gui/js/main.js @@ -395,7 +395,7 @@ function treeIDResultsToHtml( tid ){ var r = ""; _.each(tid.results, function edgeIdToHtml(v, k){ if (v[0].fastp.length != kID) return - console.log(v[0]) + //console.log(v[0]) if (v[0].fastp) { r += "
  • Effect of "+getVertexParent(k)+" on " + k + ":
    " if (v[0].instrument) { @@ -428,7 +428,9 @@ function displayTreeIDInfo(){ var warnings = [] if( Model.dag.getSources().length != 0 || Model.dag.getTargets().length != 0 ){ - warnings = ["Do not mark exposure and outcome nodes for TreeID. TreeID tests for each node whether it and its parent are identifiable as outcome and exposure nodes.

    "] + warnings = ["

    Exposure and outcome nodes are ignored for TreeID."+ + "Instead, TreeID tests identifiability of every direct effect (path coefficient) "+ + "simultaneously.

    "] document.getElementById("causal_effect").innerHTML = "

    "+warnings[0]+"

    " } var tid diff --git a/jslib/graph/GraphAnalyzer.js b/jslib/graph/GraphAnalyzer.js index 156ac32..a1ec2f0 100644 --- a/jslib/graph/GraphAnalyzer.js +++ b/jslib/graph/GraphAnalyzer.js @@ -2126,7 +2126,7 @@ var GraphAnalyzer = { } var res = {"results": IDobject} - if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. The algorithm assumes all nodes except a certain root node have exactly one parent, and ignores nodes with more parents"] + if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. TreeID assumes all nodes except one (the root) have exactly one parent, and ignores nodes with more parents."] return res } } diff --git a/r/inst/js/dagitty-alg.js b/r/inst/js/dagitty-alg.js index 0ac3cf5..74ea336 100644 --- a/r/inst/js/dagitty-alg.js +++ b/r/inst/js/dagitty-alg.js @@ -3106,7 +3106,7 @@ var GraphAnalyzer = { } var res = {"results": IDobject} - if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. The algorithm assumes all nodes except a certain root node have exactly one parent, and ignores nodes with more parents"] + if (hasnontreenodes) res.warnings = ["Some nodes have more than one parent. TreeID assumes all nodes except one (the root) have exactly one parent, and ignores nodes with more parents."] return res } } From 0686b40e007919c949caa6e98706389f0f68a62a Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Thu, 3 Mar 2022 14:57:39 -0800 Subject: [PATCH 09/17] refactor testing to use command line --- gui/dags.html | 10 +- gui/js/dagitty.js | 167 ++- gui/js/example-dags.js | 19 +- gui/js/main.js | 60 +- jslib/graph/Graph.js | 9 +- jslib/graph/GraphAnalyzer.js | 145 +- jslib/graph/GraphParser.js | 13 +- jslib/node-post.js | 3 +- jslib/node-pre.js | 2 +- r/inst/js/dagitty-alg.js | 167 ++- test/package-lock.json | 50 + test/package.json | 9 + test/test-graphs.js | 44 +- test/test/adjustment-dags.js | 125 ++ test/test/adjustment-other.js | 40 + test/test/ancestry.js | 29 + test/test/biasing-paths.js | 70 + test/test/dseparation.js | 118 ++ test/test/graph-analysis.js | 80 ++ test/test/graph-transformations.js | 127 ++ test/test/graph-types.js | 47 + test/test/graph-validation.js | 29 + test/test/instrumental-variables.js | 64 + test/test/manipulation.js | 55 + test/test/misc.js | 729 ++++++++++ test/test/pags.js | 62 + test/test/parser.js | 233 ++++ test/test/polynomials.js | 115 ++ test/test/selection-nodes.js | 18 + test/test/separators.js | 97 ++ test/test/testable-implications.js | 72 + test/test/tetrad-analyis.js | 59 + test/test/tree-id.js | 64 + test/tests.js | 1947 --------------------------- 34 files changed, 2720 insertions(+), 2158 deletions(-) create mode 100644 test/package-lock.json create mode 100644 test/package.json create mode 100644 test/test/adjustment-dags.js create mode 100644 test/test/adjustment-other.js create mode 100644 test/test/ancestry.js create mode 100644 test/test/biasing-paths.js create mode 100644 test/test/dseparation.js create mode 100644 test/test/graph-analysis.js create mode 100644 test/test/graph-transformations.js create mode 100644 test/test/graph-types.js create mode 100644 test/test/graph-validation.js create mode 100644 test/test/instrumental-variables.js create mode 100644 test/test/manipulation.js create mode 100644 test/test/misc.js create mode 100644 test/test/pags.js create mode 100644 test/test/parser.js create mode 100644 test/test/polynomials.js create mode 100644 test/test/selection-nodes.js create mode 100644 test/test/separators.js create mode 100644 test/test/testable-implications.js create mode 100644 test/test/tetrad-analyis.js create mode 100644 test/test/tree-id.js diff --git a/gui/dags.html b/gui/dags.html index 369b653..d1e0fa9 100644 --- a/gui/dags.html +++ b/gui/dags.html @@ -368,10 +368,12 @@

    -

    +

    Causal effect identification

    + +

    +
    -
    - -
    +
    +

    Testable implications

    diff --git a/gui/js/dagitty.js b/gui/js/dagitty.js index 5f79145..5bce39d 100644 --- a/gui/js/dagitty.js +++ b/gui/js/dagitty.js @@ -132,14 +132,16 @@ _.extend( Hash.prototype, { * be moved to either GraphAnalyzer, GraphTransform or GraphSerializer in the future. */ -/* globals _, Class, Hash, GraphSerializer */ +/* globals _, Class, GraphParser, Hash, GraphSerializer */ var Graph = Class.extend({ // additional getter and setter methods for these properties are mixed in below, // see code after definition of this class managed_vertex_property_names : ["source","target","adjustedNode", "latentNode","selectedNode"], - init : function(){ + + /** @param s : allows Graph to be constructed directly from Dot statements */ + init : function( s ){ this.vertices = new Hash() this.edges = [] this.type = "digraph" @@ -149,6 +151,9 @@ var Graph = Class.extend({ _.each(this.managed_vertex_property_names,function(p){ this.managed_vertex_properties[p] = new Hash() },this) + if( typeof s === "string" ){ + GraphParser.parseDot( s, this ) + } }, getBoundingBox : function(){ @@ -1149,10 +1154,10 @@ var GraphAnalyzer = { if( v1_pre == v2_pre ){ if( v1_pre == "up_" ) trek_monomials[i].push( - pars(g.getEdge(v2_id,v1_id,Graph.Edgetype.Directed),"b")) - else - trek_monomials[i].push( - pars(g.getEdge(v1_id,v2_id,Graph.Edgetype.Directed),"b")) + pars(g.getEdge(v2_id,v1_id,Graph.Edgetype.Directed),"b")) + else + trek_monomials[i].push( + pars(g.getEdge(v1_id,v2_id,Graph.Edgetype.Directed),"b")) } else { if( v1_id == v2_id ){ if( !standardized ){ @@ -1366,9 +1371,13 @@ var GraphAnalyzer = { return i }, - isAdjustmentSet : function( g, Z ){ + /** + * Determines whether Z is a valid adjustment set in g, with possible selection bias + * due to conditioning on nodes S. + */ + isAdjustmentSet : function( g, Z, S ){ var gtype = g.getType() - Z = _.map( Z, g.getVertex, g ) + var Zg = _.map( Z, g.getVertex, g ) if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute adjustment sets for graph of type "+gtype ) } @@ -1376,12 +1385,18 @@ var GraphAnalyzer = { return false } - if( _.intersection( this.dpcp(g), Z ).length > 0 ){ + if( _.intersection( this.dpcp(g), Zg ).length > 0 ){ return false } var gbd = GraphTransformer.backDoorGraph(g) - Z = _.map( Z, gbd.getVertex, gbd ) - return !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Z ) + var Zgbd = _.map( Zg, gbd.getVertex, gbd ) + var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd ) + if( S && S.length > 0 ){ + var Sg = _.map( S, g.getVertex, g ) + r = r && !this.dConnected( g, Zg, Sg, [] ) + r = r && !this.dConnected( g, g.getTargets(), Sg, g.getSources().concat( Zg ) ) + } + return r }, listMsasTotalEffect : function( g, must, must_not, max_nr ){ @@ -1400,7 +1415,7 @@ var GraphAnalyzer = { var gam = GraphTransformer.moralGraph( GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph(g) ) ) + GraphTransformer.backDoorGraph(g) ) ) if( must ) adjusted_nodes = adjusted_nodes.concat( must ) @@ -1514,7 +1529,7 @@ var GraphAnalyzer = { var vv = g2.vertices.values() // this ignores adjusted vertices for now g2.removeAllAdjustedNodes() - var n = 0 + var n = 0, i for( i = 0 ; i < vv.length ; i ++ ){ for( var j = i+1 ; j < vv.length ; j ++ ){ if( !g2.isLatentNode( vv[i] ) && !g2.isLatentNode( vv[j] ) @@ -1546,7 +1561,7 @@ var GraphAnalyzer = { var cpdag = GraphTransformer.dependencyGraph2CPDAG( g ), p1, p2 if( g.edges.all( function( e ){ if( typeof cpdag.getEdge( e.v1.id, e.v2.id, - Graph.Edgetype.Undirected ) == "undefined" ){ + Graph.Edgetype.Undirected ) == "undefined" ){ p1 = cpdag.getVertex(e.v1.id).getParents().pluck("id") p2 = cpdag.getVertex(e.v2.id).getParents().pluck("id") if( !p1.include(e.v2.id) && !p2.include(e.v1.id) @@ -1663,9 +1678,9 @@ var GraphAnalyzer = { intermediates : function( g ){ return _.chain( g.descendantsOf( g.getSources() )) - .intersection( g.ancestorsOf( g.getTargets() ) ) - .difference( g.getSources() ) - .difference( g.getTargets() ).value() + .intersection( g.ancestorsOf( g.getTargets() ) ) + .difference( g.getSources() ) + .difference( g.getTargets() ).value() }, /** @@ -1845,27 +1860,46 @@ var GraphAnalyzer = { /** d-Separation test via Shachter's "Bayes-Ball" BFS. * (actually, implements m-separation which is however not guaranteed to be meaningful * in all mixed graphs). + * If X is empty, always returns "false". * If Y is nonempty, returns true iff X and Z are d-separated given Z. * If Y is empty ([]), return the set of vertices that are d-connected * to X given Z. */ dConnected : function( g, X, Y, Z, AnZ ){ var go = g + if( g.getType() == "pag" ){ g = GraphTransformer.pagToPdag( g ) - X = g.getVertex(X) - Y = g.getVertex(Y) - Z = g.getVertex(Z) - if( typeof AnZ !== 'undefined' ){ AnZ = g.getVertex( AnZ ) } } + + if( X.length == 0 ){ + if( Y.length == 0 ){ + return [] + } else { + false + } + } + + X = _.map( X, g.getVertex, g ) + Y = _.map( Y, g.getVertex, g ) + + if( typeof Z == "undefined" ){ + Z = [] + } else { + Z = _.map( Z, g.getVertex, g ) + } + if( typeof AnZ == "undefined" ){ + AnZ = g.ancestorsOf( Z ) + } else { + AnZ = _.map( AnZ, g.getVertex, g ) + } + var forward_queue = [] var backward_queue = [] var forward_visited ={} var backward_visited = {} var i, Y_ids = {}, Z_ids = {}, AnZ_ids = {}, v, vv - if( typeof AnZ == "undefined" ){ - AnZ = g.ancestorsOf( Z ) - } + for( i = 0 ; i < X.length ; i ++ ){ backward_queue.push( X[i] ) } @@ -1936,7 +1970,7 @@ var GraphAnalyzer = { }, ancestralInstrument : function( g, x, y, z, - g_bd, de_y ){ + g_bd, de_y ){ if( arguments.length < 5 ){ g_bd = GraphTransformer.backDoorGraph( g, [x], [y] ) } @@ -2115,11 +2149,11 @@ var GraphAnalyzer = { } } } - var vv = g.vertices.values() - for( var j = 0 ; j < vv.length ; j ++ ){ + var vv = g.vertices.values(), j + for( j = 0 ; j < vv.length ; j ++ ){ topological_index[vv[j].id] = 0 } - for( var j = 0 ; j < vv.length ; j ++ ){ + for( j = 0 ; j < vv.length ; j ++ ){ visit( vv[j] ) } return topological_index @@ -2136,16 +2170,16 @@ var GraphAnalyzer = { g.clearTraversalInfo() _.each( adj, function(v){ Graph.Vertex.markAsVisited(v) }) } ), function(v){ - reaches_source[v.id] = true - }) + reaches_source[v.id] = true + }) _.each( g.ancestorsOf( g.getTargets(), function(){ var adj = g.getAdjustedNodes() g.clearTraversalInfo() _.each( adj, function(v){ Graph.Vertex.markAsVisited(v) }) } ), function(v){ - reaches_target[v.id] = true - }) + reaches_target[v.id] = true + }) var vv = g.vertices.values() var bn_s = topological_index[s0.id] var bn_t = topological_index[t0.id] @@ -2557,7 +2591,7 @@ var GraphAnalyzer = { containsSemiCycle: function (g){ return GraphAnalyzer.containsCycle( GraphTransformer.contractComponents(g, GraphAnalyzer.connectedComponents(g), [Graph.Edgetype.Directed]) - ) + ) }, /* Check whether the directed edge e is stronlgy protected */ @@ -2675,7 +2709,7 @@ var GraphAnalyzer = { _.each( v.outgoingEdges, function( e ){ var vc = e.v1 === v ? e.v2 : e.v1 if (e.directed == Graph.Edgetype.Bidirected) { - trek.push( e ) + trek.push( e ) visitDown( vc, trek ) trek.pop() } @@ -2808,10 +2842,10 @@ var GraphAnalyzer = { } var primes = findPrimes(3*(MPolyHelper.variableCount + 1) + 20 ) var simulated = [ - simulateNumeric(function(){ return Math.floor((1 << 52)*Math.random()) + 1 }), - simulateNumeric(function(i){ return primes[3*i+20] }) - //simulateNumeric(function(i){ return 1 }) this would be stupid, but it works - ] + simulateNumeric(function(){ return Math.floor((1 << 52)*Math.random()) + 1 }), + simulateNumeric(function(i){ return primes[3*i+20] }) + //simulateNumeric(function(i){ return 1 }) this would be stupid, but it works + ] @@ -2884,17 +2918,17 @@ var GraphAnalyzer = { var si3 = sigma[p][q] var si4 = sigma[i][q].negate() return FASTP.make( fastp.r().mul(si2).add(fastp.p().mul(si1)), fastp.s() ? fastp.t().mul(si2).add(fastp.q().mul(si1)) : null, - fastp.r().mul(si4).add(fastp.p().mul(si3)), fastp.s() ? fastp.t().mul(si4).add(fastp.q().mul(si3)) : null, - fastp.s() ) + fastp.r().mul(si4).add(fastp.p().mul(si3)), fastp.s() ? fastp.t().mul(si4).add(fastp.q().mul(si3)) : null, + fastp.s() ) } ) ID[j] = {propagate: i, - propagatePath: "propagate" in ID[i] ? ID[i].propagatePath.concat([i]) : [i], - fastp: newfastp, - propagatedMissingCycles: "propagate" in ID[i] ? ID[i].propagatedMissingCycles : ID[i].missingCycles, - oldMissingCycles: ID[j] ? ID[j].missingCycles : null - // oldPropagatedMissingCycles: ID[j] ? ID[j].propagatedMissingCycles : null, - } - propagate(j) + propagatePath: "propagate" in ID[i] ? ID[i].propagatePath.concat([i]) : [i], + fastp: newfastp, + propagatedMissingCycles: "propagate" in ID[i] ? ID[i].propagatedMissingCycles : ID[i].missingCycles, + oldMissingCycles: ID[j] ? ID[j].missingCycles : null + // oldPropagatedMissingCycles: ID[j] ? ID[j].propagatedMissingCycles : null, + } + propagate(j) } ) } @@ -2912,9 +2946,9 @@ var GraphAnalyzer = { var p = pa[i] var q = pa[j] return [ sigma[p][q], - sigma[p][j].negate(), //lambda[p][i] - sigma[i][q].negate(), //lambda[q][j] - sigma[i][j] ] + sigma[p][j].negate(), //lambda[p][i] + sigma[i][q].negate(), //lambda[q][j] + sigma[i][j] ] } function missingCycleToQuadraticEquation(cycle){ @@ -2926,7 +2960,7 @@ var GraphAnalyzer = { var C = 2 var D = 3 var k = cycle.length - 1 - // var vars = cycle.slice(0, k) + // var vars = cycle.slice(0, k) var eqs = _.map(new Array(k), function(x,i) { return bidiEquation(cycle[i], cycle[i+1]) } ) while (k > 2) { var newk = Math.ceil(k/2) @@ -2940,7 +2974,7 @@ var GraphAnalyzer = { DET2(eqs[i][C], eqs[i+1][C], eqs[i+1][A], eqs[i][D]), DET2(eqs[i][C], eqs[i+1][D], eqs[i+1][B], eqs[i][D]) ] - // newvars[j] = vars[i]?? + // newvars[j] = vars[i]?? } if (k % 2 == 1) neweqs[newk - 1] = eqs[k - 1] k = newk @@ -2949,10 +2983,10 @@ var GraphAnalyzer = { } if (k == 2) { eqs[0] = [ DET2(eqs[0][A], eqs[1][C], eqs[1][A], eqs[0][B]), - DET2(eqs[0][A], eqs[1][D], eqs[1][A], eqs[0][D]), - DET2(eqs[0][C], eqs[1][C], eqs[1][B], eqs[0][B]), - DET2(eqs[0][C], eqs[1][D], eqs[1][B], eqs[0][D]) - ] + DET2(eqs[0][A], eqs[1][D], eqs[1][A], eqs[0][D]), + DET2(eqs[0][C], eqs[1][C], eqs[1][B], eqs[0][B]), + DET2(eqs[0][C], eqs[1][D], eqs[1][B], eqs[0][D]) + ] } var a = eqs[0][0] var b = eqs[0][1].add(eqs[0][2]) @@ -2970,7 +3004,7 @@ var GraphAnalyzer = { var minus_b = b.negate() if (isZeroSigmaPoly(ss)) return [ FASTP.makeFraction(minus_b, two_a) ] return [ FASTP.make(minus_b, MINUS_ONE, two_a, ZERO, ss), - FASTP.make(minus_b, ONE, two_a, ZERO, ss) ] + FASTP.make(minus_b, ONE, two_a, ZERO, ss) ] } function fastpMightSatisfyQuadraticEquation(fastp, abc) { @@ -3000,17 +3034,17 @@ var GraphAnalyzer = { t = t.evalToBigInt(simulated[i]) var app_aqqs_bpr_bqst_crr_ctts = (a*p*p) + (a*q*q*s) + (b*p*r) + (b*q*s*t) + (c*r*r) + (c*t*t*s) - var minus_2apq_bpt_bqr_2crt = - ((2n*a*p*q) + (b*p*t) + (b*q*r) + (2n* c*r*t)) + var minus_2apq_bpt_bqr_2crt = - (((2n)*a*p*q) + (b*p*t) + (b*q*r) + ((2n)* c*r*t)) if (app_aqqs_bpr_bqst_crr_ctts < 0 && minus_2apq_bpt_bqr_2crt > 0) return false if (app_aqqs_bpr_bqst_crr_ctts > 0 && minus_2apq_bpt_bqr_2crt < 0) return false if (app_aqqs_bpr_bqst_crr_ctts != 0 && minus_2apq_bpt_bqr_2crt == 0) return false if (app_aqqs_bpr_bqst_crr_ctts == 0 && minus_2apq_bpt_bqr_2crt != 0 && s != 0) return false - if (app_aqqs_bpr_bqst_crr_ctts**2n != (minus_2apq_bpt_bqr_2crt**2n) * s) return false + if (app_aqqs_bpr_bqst_crr_ctts ** 2n != (minus_2apq_bpt_bqr_2crt**2n) * s) return false } return true -/* Symbolic approach does not work (too slow and loses sign) + /* Symbolic approach does not work (too slow and loses sign) app_aqqs_bpr_bqst_crr_ctts = ADD(MUL(a,p,p), MUL(a,q,q,s), MUL(b,p,r), MUL(b,q,s,t), MUL(c,r, r), MUL(c,t,t,s)) var TWO = MPoly("2") apq2_bpt_bqr_2crt = ADD(MUL(TWO,a,p,q), MUL(b,p,t), MUL(b,q,r), MUL(TWO, c,r,t))*/ @@ -3301,10 +3335,17 @@ GraphLayouter.Spring.prototype = { var GraphParser = { VALIDATE_GRAPH_STRUCTURE : false, - parseDot : function( code ){ + parseDot : function( code, g ){ "use strict" + code = code.trim() + var isdot = code.trim().match( /^(digraph|graph|dag|pdag|mag|pag)(\s+\w+)?\s*\{([\s\S]*)\}$/mi ) + if( !isdot || (isdot.length <= 1) ){ + code = "dag{ " + code + "}" + } var ast = GraphDotParser.parse( code ) - var g = new Graph() + if( typeof g === "undefined" ){ + g = new Graph() + } this.parseDotStatementArray( ast.statements, g ) g.setType( ast.type ) if( ast.name ){ g.setName( ast.name ) } @@ -3644,7 +3685,7 @@ var GraphParser = { var hasarrow = firstarg.match( /(->|<->|<-)/mi ) // allow users to omit explicit "dag{ ... }" if at least one arrow is also specified if( hasarrow && hasarrow.length >= 1 ){ - return this.parseDot( "dag{"+firstarg+"}" ) + return this.parseDot( firstarg ) } else { return this.parseAdjacencyList( adjacencyListOrMatrix, vertexLabelsAndWeights ) } diff --git a/gui/js/example-dags.js b/gui/js/example-dags.js index b4f87fb..ae5978e 100644 --- a/gui/js/example-dags.js +++ b/gui/js/example-dags.js @@ -1,5 +1,5 @@ /* DAGitty - a browser-based software for causal modelling and analysis - Copyright (C) 2010, 2017 Johannes Textor + Copyright (C) 2010, 2017, 2022 Johannes Textor This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License @@ -15,7 +15,20 @@ along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -var examples = [ + +// if the module has no dependencies, the above pattern can be simplified to +(function (root, factory) { + if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + // Browser globals (root is window) + root.examples = factory(); + } +}(typeof self !== 'undefined' ? self : this, function () { + return [ { d : "dag { \n"+ "bb=\"-3,-0.5,2,1.2\"" + @@ -369,3 +382,5 @@ Thist -> TCI l: "Didelez et al, 2010" } ]; + +})); diff --git a/gui/js/main.js b/gui/js/main.js index 317eea6..c35f509 100644 --- a/gui/js/main.js +++ b/gui/js/main.js @@ -1,5 +1,5 @@ /* DAGitty - a browser-based software for causal modelling and analysis -* Copyright (C) 2010,2011,2017 Johannes Textor +* Copyright (C) 2010-2022 Johannes Textor * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -213,11 +213,11 @@ function setsToHTML( sets ){ } function causalEffectEstimates(){ - if( Model.dag.getSelectedNodes().length > 0 ){ + /*if( Model.dag.getSelectedNodes().length > 0 ){ displayCausalMsg("I cannot determine causal effects for DAGs with selection nodes."); return - } + }*/ if( GraphAnalyzer.containsCycle( Model.dag ) ){ - displayCausalMsg("I cannot determine causal effects for cyclic models."); return + displayCausalMsg("Can't determine causal effects for cyclic models."); return } switch( document.getElementById("causal_effect_kind").value ){ case "adj_total" : @@ -263,11 +263,57 @@ function displayCausalMsg( wh ){ } function displayAdjustmentInfo( kind ){ - var adjusted_nodes = Model.dag.getAdjustedNodes(); - var html_adjustment = ""; + let g = Model.dag + + let adjusted_nodes = g.getAdjustedNodes(); + let html_adjustment = ""; if( kind != "total" ){ kind = "direct"; } + + let exposures = _.pluck(g.getSources(),'id').sort() + let outcomes = _.pluck(g.getTargets(),'id').sort() + let adjusted = _.pluck(g.getAdjustedNodes(),'id').sort() + let selected = _.pluck(g.getSelectedNodes(),'id').sort() + + + let exposures_list = _.pluck(g.getSources(),'id').sort() + let outcomes_list = _.pluck(g.getTargets(),'id').sort() + + let exposures_el = document.createElement( "p" ) + exposures_el.innerText = "Exposure"+(exposures.length > 1?"s: ":": ")+exposures_list.join(",") + + let outcomes_el = document.createElement( "p" ) + outcomes_el.innerText = "Outcome"+(outcomes.length > 1?"s: ":": ")+outcomes_list.join(",") + + let tgt_el = document.getElementById("causal_effect") + + tgt_el.replaceChildren( exposures_el, outcomes_el ) + + if( exposures.length == 0 || outcomes.length == 0 ){ + return + } + + if( adjusted.length > 0 ){ + let adjusted_el = document.createElement( "p" ) + adjusted_el.innerText = "Adjusted: "+adjusted.join(",") + tgt_el.appendChild( adjusted_el ) + } + + if( selected.length > 0 ){ + let selected_el = document.createElement( "p" ) + selected_el.innerText = "Selected: "+selected.join(",") + tgt_el.appendChild( selected_el ) + } + + + return + + + if( false ){ + let adjusted_nodes_html = _.pluck(adjusted_nodes,'id').sort().join(", ") + + if( adjusted_nodes.length > 0 ){ html_adjustment = " containing "+_.pluck(adjusted_nodes,'id').sort().join(", "); } @@ -311,6 +357,8 @@ function displayAdjustmentInfo( kind ){ showMsas( "direct", GraphAnalyzer.listMsasDirectEffect( Model.dag ), html_adjustment ); } + + } } diff --git a/jslib/graph/Graph.js b/jslib/graph/Graph.js index 0470ce4..6c4011f 100644 --- a/jslib/graph/Graph.js +++ b/jslib/graph/Graph.js @@ -5,14 +5,16 @@ * be moved to either GraphAnalyzer, GraphTransform or GraphSerializer in the future. */ -/* globals _, Class, Hash, GraphSerializer */ +/* globals _, Class, GraphParser, Hash, GraphSerializer */ var Graph = Class.extend({ // additional getter and setter methods for these properties are mixed in below, // see code after definition of this class managed_vertex_property_names : ["source","target","adjustedNode", "latentNode","selectedNode"], - init : function(){ + + /** @param s : allows Graph to be constructed directly from Dot statements */ + init : function( s ){ this.vertices = new Hash() this.edges = [] this.type = "digraph" @@ -22,6 +24,9 @@ var Graph = Class.extend({ _.each(this.managed_vertex_property_names,function(p){ this.managed_vertex_properties[p] = new Hash() },this) + if( typeof s === "string" ){ + GraphParser.parseDot( s, this ) + } }, getBoundingBox : function(){ diff --git a/jslib/graph/GraphAnalyzer.js b/jslib/graph/GraphAnalyzer.js index a1ec2f0..22a9da6 100644 --- a/jslib/graph/GraphAnalyzer.js +++ b/jslib/graph/GraphAnalyzer.js @@ -164,10 +164,10 @@ var GraphAnalyzer = { if( v1_pre == v2_pre ){ if( v1_pre == "up_" ) trek_monomials[i].push( - pars(g.getEdge(v2_id,v1_id,Graph.Edgetype.Directed),"b")) - else - trek_monomials[i].push( - pars(g.getEdge(v1_id,v2_id,Graph.Edgetype.Directed),"b")) + pars(g.getEdge(v2_id,v1_id,Graph.Edgetype.Directed),"b")) + else + trek_monomials[i].push( + pars(g.getEdge(v1_id,v2_id,Graph.Edgetype.Directed),"b")) } else { if( v1_id == v2_id ){ if( !standardized ){ @@ -381,9 +381,13 @@ var GraphAnalyzer = { return i }, - isAdjustmentSet : function( g, Z ){ + /** + * Determines whether Z is a valid adjustment set in g, with possible selection bias + * due to conditioning on nodes S. + */ + isAdjustmentSet : function( g, Z, S ){ var gtype = g.getType() - Z = _.map( Z, g.getVertex, g ) + var Zg = _.map( Z, g.getVertex, g ) if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute adjustment sets for graph of type "+gtype ) } @@ -391,12 +395,18 @@ var GraphAnalyzer = { return false } - if( _.intersection( this.dpcp(g), Z ).length > 0 ){ + if( _.intersection( this.dpcp(g), Zg ).length > 0 ){ return false } var gbd = GraphTransformer.backDoorGraph(g) - Z = _.map( Z, gbd.getVertex, gbd ) - return !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Z ) + var Zgbd = _.map( Zg, gbd.getVertex, gbd ) + var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd ) + if( S && S.length > 0 ){ + var Sg = _.map( S, g.getVertex, g ) + r = r && !this.dConnected( g, Zg, Sg, [] ) + r = r && !this.dConnected( g, g.getTargets(), Sg, g.getSources().concat( Zg ) ) + } + return r }, listMsasTotalEffect : function( g, must, must_not, max_nr ){ @@ -415,7 +425,7 @@ var GraphAnalyzer = { var gam = GraphTransformer.moralGraph( GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph(g) ) ) + GraphTransformer.backDoorGraph(g) ) ) if( must ) adjusted_nodes = adjusted_nodes.concat( must ) @@ -529,7 +539,7 @@ var GraphAnalyzer = { var vv = g2.vertices.values() // this ignores adjusted vertices for now g2.removeAllAdjustedNodes() - var n = 0 + var n = 0, i for( i = 0 ; i < vv.length ; i ++ ){ for( var j = i+1 ; j < vv.length ; j ++ ){ if( !g2.isLatentNode( vv[i] ) && !g2.isLatentNode( vv[j] ) @@ -561,7 +571,7 @@ var GraphAnalyzer = { var cpdag = GraphTransformer.dependencyGraph2CPDAG( g ), p1, p2 if( g.edges.all( function( e ){ if( typeof cpdag.getEdge( e.v1.id, e.v2.id, - Graph.Edgetype.Undirected ) == "undefined" ){ + Graph.Edgetype.Undirected ) == "undefined" ){ p1 = cpdag.getVertex(e.v1.id).getParents().pluck("id") p2 = cpdag.getVertex(e.v2.id).getParents().pluck("id") if( !p1.include(e.v2.id) && !p2.include(e.v1.id) @@ -678,9 +688,9 @@ var GraphAnalyzer = { intermediates : function( g ){ return _.chain( g.descendantsOf( g.getSources() )) - .intersection( g.ancestorsOf( g.getTargets() ) ) - .difference( g.getSources() ) - .difference( g.getTargets() ).value() + .intersection( g.ancestorsOf( g.getTargets() ) ) + .difference( g.getSources() ) + .difference( g.getTargets() ).value() }, /** @@ -860,27 +870,46 @@ var GraphAnalyzer = { /** d-Separation test via Shachter's "Bayes-Ball" BFS. * (actually, implements m-separation which is however not guaranteed to be meaningful * in all mixed graphs). + * If X is empty, always returns "false". * If Y is nonempty, returns true iff X and Z are d-separated given Z. * If Y is empty ([]), return the set of vertices that are d-connected * to X given Z. */ dConnected : function( g, X, Y, Z, AnZ ){ var go = g + if( g.getType() == "pag" ){ g = GraphTransformer.pagToPdag( g ) - X = g.getVertex(X) - Y = g.getVertex(Y) - Z = g.getVertex(Z) - if( typeof AnZ !== 'undefined' ){ AnZ = g.getVertex( AnZ ) } } + + if( X.length == 0 ){ + if( Y.length == 0 ){ + return [] + } else { + false + } + } + + X = _.map( X, g.getVertex, g ) + Y = _.map( Y, g.getVertex, g ) + + if( typeof Z == "undefined" ){ + Z = [] + } else { + Z = _.map( Z, g.getVertex, g ) + } + if( typeof AnZ == "undefined" ){ + AnZ = g.ancestorsOf( Z ) + } else { + AnZ = _.map( AnZ, g.getVertex, g ) + } + var forward_queue = [] var backward_queue = [] var forward_visited ={} var backward_visited = {} var i, Y_ids = {}, Z_ids = {}, AnZ_ids = {}, v, vv - if( typeof AnZ == "undefined" ){ - AnZ = g.ancestorsOf( Z ) - } + for( i = 0 ; i < X.length ; i ++ ){ backward_queue.push( X[i] ) } @@ -951,7 +980,7 @@ var GraphAnalyzer = { }, ancestralInstrument : function( g, x, y, z, - g_bd, de_y ){ + g_bd, de_y ){ if( arguments.length < 5 ){ g_bd = GraphTransformer.backDoorGraph( g, [x], [y] ) } @@ -1130,11 +1159,11 @@ var GraphAnalyzer = { } } } - var vv = g.vertices.values() - for( var j = 0 ; j < vv.length ; j ++ ){ + var vv = g.vertices.values(), j + for( j = 0 ; j < vv.length ; j ++ ){ topological_index[vv[j].id] = 0 } - for( var j = 0 ; j < vv.length ; j ++ ){ + for( j = 0 ; j < vv.length ; j ++ ){ visit( vv[j] ) } return topological_index @@ -1151,16 +1180,16 @@ var GraphAnalyzer = { g.clearTraversalInfo() _.each( adj, function(v){ Graph.Vertex.markAsVisited(v) }) } ), function(v){ - reaches_source[v.id] = true - }) + reaches_source[v.id] = true + }) _.each( g.ancestorsOf( g.getTargets(), function(){ var adj = g.getAdjustedNodes() g.clearTraversalInfo() _.each( adj, function(v){ Graph.Vertex.markAsVisited(v) }) } ), function(v){ - reaches_target[v.id] = true - }) + reaches_target[v.id] = true + }) var vv = g.vertices.values() var bn_s = topological_index[s0.id] var bn_t = topological_index[t0.id] @@ -1572,7 +1601,7 @@ var GraphAnalyzer = { containsSemiCycle: function (g){ return GraphAnalyzer.containsCycle( GraphTransformer.contractComponents(g, GraphAnalyzer.connectedComponents(g), [Graph.Edgetype.Directed]) - ) + ) }, /* Check whether the directed edge e is stronlgy protected */ @@ -1690,7 +1719,7 @@ var GraphAnalyzer = { _.each( v.outgoingEdges, function( e ){ var vc = e.v1 === v ? e.v2 : e.v1 if (e.directed == Graph.Edgetype.Bidirected) { - trek.push( e ) + trek.push( e ) visitDown( vc, trek ) trek.pop() } @@ -1823,10 +1852,10 @@ var GraphAnalyzer = { } var primes = findPrimes(3*(MPolyHelper.variableCount + 1) + 20 ) var simulated = [ - simulateNumeric(function(){ return Math.floor((1 << 52)*Math.random()) + 1 }), - simulateNumeric(function(i){ return primes[3*i+20] }) - //simulateNumeric(function(i){ return 1 }) this would be stupid, but it works - ] + simulateNumeric(function(){ return Math.floor((1 << 52)*Math.random()) + 1 }), + simulateNumeric(function(i){ return primes[3*i+20] }) + //simulateNumeric(function(i){ return 1 }) this would be stupid, but it works + ] @@ -1899,17 +1928,17 @@ var GraphAnalyzer = { var si3 = sigma[p][q] var si4 = sigma[i][q].negate() return FASTP.make( fastp.r().mul(si2).add(fastp.p().mul(si1)), fastp.s() ? fastp.t().mul(si2).add(fastp.q().mul(si1)) : null, - fastp.r().mul(si4).add(fastp.p().mul(si3)), fastp.s() ? fastp.t().mul(si4).add(fastp.q().mul(si3)) : null, - fastp.s() ) + fastp.r().mul(si4).add(fastp.p().mul(si3)), fastp.s() ? fastp.t().mul(si4).add(fastp.q().mul(si3)) : null, + fastp.s() ) } ) ID[j] = {propagate: i, - propagatePath: "propagate" in ID[i] ? ID[i].propagatePath.concat([i]) : [i], - fastp: newfastp, - propagatedMissingCycles: "propagate" in ID[i] ? ID[i].propagatedMissingCycles : ID[i].missingCycles, - oldMissingCycles: ID[j] ? ID[j].missingCycles : null - // oldPropagatedMissingCycles: ID[j] ? ID[j].propagatedMissingCycles : null, - } - propagate(j) + propagatePath: "propagate" in ID[i] ? ID[i].propagatePath.concat([i]) : [i], + fastp: newfastp, + propagatedMissingCycles: "propagate" in ID[i] ? ID[i].propagatedMissingCycles : ID[i].missingCycles, + oldMissingCycles: ID[j] ? ID[j].missingCycles : null + // oldPropagatedMissingCycles: ID[j] ? ID[j].propagatedMissingCycles : null, + } + propagate(j) } ) } @@ -1927,9 +1956,9 @@ var GraphAnalyzer = { var p = pa[i] var q = pa[j] return [ sigma[p][q], - sigma[p][j].negate(), //lambda[p][i] - sigma[i][q].negate(), //lambda[q][j] - sigma[i][j] ] + sigma[p][j].negate(), //lambda[p][i] + sigma[i][q].negate(), //lambda[q][j] + sigma[i][j] ] } function missingCycleToQuadraticEquation(cycle){ @@ -1941,7 +1970,7 @@ var GraphAnalyzer = { var C = 2 var D = 3 var k = cycle.length - 1 - // var vars = cycle.slice(0, k) + // var vars = cycle.slice(0, k) var eqs = _.map(new Array(k), function(x,i) { return bidiEquation(cycle[i], cycle[i+1]) } ) while (k > 2) { var newk = Math.ceil(k/2) @@ -1955,7 +1984,7 @@ var GraphAnalyzer = { DET2(eqs[i][C], eqs[i+1][C], eqs[i+1][A], eqs[i][D]), DET2(eqs[i][C], eqs[i+1][D], eqs[i+1][B], eqs[i][D]) ] - // newvars[j] = vars[i]?? + // newvars[j] = vars[i]?? } if (k % 2 == 1) neweqs[newk - 1] = eqs[k - 1] k = newk @@ -1964,10 +1993,10 @@ var GraphAnalyzer = { } if (k == 2) { eqs[0] = [ DET2(eqs[0][A], eqs[1][C], eqs[1][A], eqs[0][B]), - DET2(eqs[0][A], eqs[1][D], eqs[1][A], eqs[0][D]), - DET2(eqs[0][C], eqs[1][C], eqs[1][B], eqs[0][B]), - DET2(eqs[0][C], eqs[1][D], eqs[1][B], eqs[0][D]) - ] + DET2(eqs[0][A], eqs[1][D], eqs[1][A], eqs[0][D]), + DET2(eqs[0][C], eqs[1][C], eqs[1][B], eqs[0][B]), + DET2(eqs[0][C], eqs[1][D], eqs[1][B], eqs[0][D]) + ] } var a = eqs[0][0] var b = eqs[0][1].add(eqs[0][2]) @@ -1985,7 +2014,7 @@ var GraphAnalyzer = { var minus_b = b.negate() if (isZeroSigmaPoly(ss)) return [ FASTP.makeFraction(minus_b, two_a) ] return [ FASTP.make(minus_b, MINUS_ONE, two_a, ZERO, ss), - FASTP.make(minus_b, ONE, two_a, ZERO, ss) ] + FASTP.make(minus_b, ONE, two_a, ZERO, ss) ] } function fastpMightSatisfyQuadraticEquation(fastp, abc) { @@ -2015,17 +2044,17 @@ var GraphAnalyzer = { t = t.evalToBigInt(simulated[i]) var app_aqqs_bpr_bqst_crr_ctts = (a*p*p) + (a*q*q*s) + (b*p*r) + (b*q*s*t) + (c*r*r) + (c*t*t*s) - var minus_2apq_bpt_bqr_2crt = - ((2n*a*p*q) + (b*p*t) + (b*q*r) + (2n* c*r*t)) + var minus_2apq_bpt_bqr_2crt = - (((2n)*a*p*q) + (b*p*t) + (b*q*r) + ((2n)* c*r*t)) if (app_aqqs_bpr_bqst_crr_ctts < 0 && minus_2apq_bpt_bqr_2crt > 0) return false if (app_aqqs_bpr_bqst_crr_ctts > 0 && minus_2apq_bpt_bqr_2crt < 0) return false if (app_aqqs_bpr_bqst_crr_ctts != 0 && minus_2apq_bpt_bqr_2crt == 0) return false if (app_aqqs_bpr_bqst_crr_ctts == 0 && minus_2apq_bpt_bqr_2crt != 0 && s != 0) return false - if (app_aqqs_bpr_bqst_crr_ctts**2n != (minus_2apq_bpt_bqr_2crt**2n) * s) return false + if (app_aqqs_bpr_bqst_crr_ctts ** 2n != (minus_2apq_bpt_bqr_2crt**2n) * s) return false } return true -/* Symbolic approach does not work (too slow and loses sign) + /* Symbolic approach does not work (too slow and loses sign) app_aqqs_bpr_bqst_crr_ctts = ADD(MUL(a,p,p), MUL(a,q,q,s), MUL(b,p,r), MUL(b,q,s,t), MUL(c,r, r), MUL(c,t,t,s)) var TWO = MPoly("2") apq2_bpt_bqr_2crt = ADD(MUL(TWO,a,p,q), MUL(b,p,t), MUL(b,q,r), MUL(TWO, c,r,t))*/ diff --git a/jslib/graph/GraphParser.js b/jslib/graph/GraphParser.js index 689d8f5..4a20040 100644 --- a/jslib/graph/GraphParser.js +++ b/jslib/graph/GraphParser.js @@ -21,10 +21,17 @@ var GraphParser = { VALIDATE_GRAPH_STRUCTURE : false, - parseDot : function( code ){ + parseDot : function( code, g ){ "use strict" + code = code.trim() + var isdot = code.trim().match( /^(digraph|graph|dag|pdag|mag|pag)(\s+\w+)?\s*\{([\s\S]*)\}$/mi ) + if( !isdot || (isdot.length <= 1) ){ + code = "dag{ " + code + "}" + } var ast = GraphDotParser.parse( code ) - var g = new Graph() + if( typeof g === "undefined" ){ + g = new Graph() + } this.parseDotStatementArray( ast.statements, g ) g.setType( ast.type ) if( ast.name ){ g.setName( ast.name ) } @@ -364,7 +371,7 @@ var GraphParser = { var hasarrow = firstarg.match( /(->|<->|<-)/mi ) // allow users to omit explicit "dag{ ... }" if at least one arrow is also specified if( hasarrow && hasarrow.length >= 1 ){ - return this.parseDot( "dag{"+firstarg+"}" ) + return this.parseDot( firstarg ) } else { return this.parseAdjacencyList( adjacencyListOrMatrix, vertexLabelsAndWeights ) } diff --git a/jslib/node-post.js b/jslib/node-post.js index fb991f5..c17296d 100644 --- a/jslib/node-post.js +++ b/jslib/node-post.js @@ -9,7 +9,8 @@ module.exports = Object.assign( module.exports, { GraphAnalyzer: GraphAnalyzer, GraphParser: GraphParser, GraphTransformer: GraphTransformer, - GraphSerializer : GraphSerializer + GraphSerializer : GraphSerializer, + MPoly : MPoly } ) diff --git a/jslib/node-pre.js b/jslib/node-pre.js index 78784a5..de38c47 100644 --- a/jslib/node-pre.js +++ b/jslib/node-pre.js @@ -1,4 +1,4 @@ -var _ = require("underscore") +const _ = require("underscore") diff --git a/r/inst/js/dagitty-alg.js b/r/inst/js/dagitty-alg.js index 74ea336..b260532 100644 --- a/r/inst/js/dagitty-alg.js +++ b/r/inst/js/dagitty-alg.js @@ -127,14 +127,16 @@ _.extend( Hash.prototype, { * be moved to either GraphAnalyzer, GraphTransform or GraphSerializer in the future. */ -/* globals _, Class, Hash, GraphSerializer */ +/* globals _, Class, GraphParser, Hash, GraphSerializer */ var Graph = Class.extend({ // additional getter and setter methods for these properties are mixed in below, // see code after definition of this class managed_vertex_property_names : ["source","target","adjustedNode", "latentNode","selectedNode"], - init : function(){ + + /** @param s : allows Graph to be constructed directly from Dot statements */ + init : function( s ){ this.vertices = new Hash() this.edges = [] this.type = "digraph" @@ -144,6 +146,9 @@ var Graph = Class.extend({ _.each(this.managed_vertex_property_names,function(p){ this.managed_vertex_properties[p] = new Hash() },this) + if( typeof s === "string" ){ + GraphParser.parseDot( s, this ) + } }, getBoundingBox : function(){ @@ -1144,10 +1149,10 @@ var GraphAnalyzer = { if( v1_pre == v2_pre ){ if( v1_pre == "up_" ) trek_monomials[i].push( - pars(g.getEdge(v2_id,v1_id,Graph.Edgetype.Directed),"b")) - else - trek_monomials[i].push( - pars(g.getEdge(v1_id,v2_id,Graph.Edgetype.Directed),"b")) + pars(g.getEdge(v2_id,v1_id,Graph.Edgetype.Directed),"b")) + else + trek_monomials[i].push( + pars(g.getEdge(v1_id,v2_id,Graph.Edgetype.Directed),"b")) } else { if( v1_id == v2_id ){ if( !standardized ){ @@ -1361,9 +1366,13 @@ var GraphAnalyzer = { return i }, - isAdjustmentSet : function( g, Z ){ + /** + * Determines whether Z is a valid adjustment set in g, with possible selection bias + * due to conditioning on nodes S. + */ + isAdjustmentSet : function( g, Z, S ){ var gtype = g.getType() - Z = _.map( Z, g.getVertex, g ) + var Zg = _.map( Z, g.getVertex, g ) if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute adjustment sets for graph of type "+gtype ) } @@ -1371,12 +1380,18 @@ var GraphAnalyzer = { return false } - if( _.intersection( this.dpcp(g), Z ).length > 0 ){ + if( _.intersection( this.dpcp(g), Zg ).length > 0 ){ return false } var gbd = GraphTransformer.backDoorGraph(g) - Z = _.map( Z, gbd.getVertex, gbd ) - return !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Z ) + var Zgbd = _.map( Zg, gbd.getVertex, gbd ) + var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd ) + if( S && S.length > 0 ){ + var Sg = _.map( S, g.getVertex, g ) + r = r && !this.dConnected( g, Zg, Sg, [] ) + r = r && !this.dConnected( g, g.getTargets(), Sg, g.getSources().concat( Zg ) ) + } + return r }, listMsasTotalEffect : function( g, must, must_not, max_nr ){ @@ -1395,7 +1410,7 @@ var GraphAnalyzer = { var gam = GraphTransformer.moralGraph( GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph(g) ) ) + GraphTransformer.backDoorGraph(g) ) ) if( must ) adjusted_nodes = adjusted_nodes.concat( must ) @@ -1509,7 +1524,7 @@ var GraphAnalyzer = { var vv = g2.vertices.values() // this ignores adjusted vertices for now g2.removeAllAdjustedNodes() - var n = 0 + var n = 0, i for( i = 0 ; i < vv.length ; i ++ ){ for( var j = i+1 ; j < vv.length ; j ++ ){ if( !g2.isLatentNode( vv[i] ) && !g2.isLatentNode( vv[j] ) @@ -1541,7 +1556,7 @@ var GraphAnalyzer = { var cpdag = GraphTransformer.dependencyGraph2CPDAG( g ), p1, p2 if( g.edges.all( function( e ){ if( typeof cpdag.getEdge( e.v1.id, e.v2.id, - Graph.Edgetype.Undirected ) == "undefined" ){ + Graph.Edgetype.Undirected ) == "undefined" ){ p1 = cpdag.getVertex(e.v1.id).getParents().pluck("id") p2 = cpdag.getVertex(e.v2.id).getParents().pluck("id") if( !p1.include(e.v2.id) && !p2.include(e.v1.id) @@ -1658,9 +1673,9 @@ var GraphAnalyzer = { intermediates : function( g ){ return _.chain( g.descendantsOf( g.getSources() )) - .intersection( g.ancestorsOf( g.getTargets() ) ) - .difference( g.getSources() ) - .difference( g.getTargets() ).value() + .intersection( g.ancestorsOf( g.getTargets() ) ) + .difference( g.getSources() ) + .difference( g.getTargets() ).value() }, /** @@ -1840,27 +1855,46 @@ var GraphAnalyzer = { /** d-Separation test via Shachter's "Bayes-Ball" BFS. * (actually, implements m-separation which is however not guaranteed to be meaningful * in all mixed graphs). + * If X is empty, always returns "false". * If Y is nonempty, returns true iff X and Z are d-separated given Z. * If Y is empty ([]), return the set of vertices that are d-connected * to X given Z. */ dConnected : function( g, X, Y, Z, AnZ ){ var go = g + if( g.getType() == "pag" ){ g = GraphTransformer.pagToPdag( g ) - X = g.getVertex(X) - Y = g.getVertex(Y) - Z = g.getVertex(Z) - if( typeof AnZ !== 'undefined' ){ AnZ = g.getVertex( AnZ ) } } + + if( X.length == 0 ){ + if( Y.length == 0 ){ + return [] + } else { + false + } + } + + X = _.map( X, g.getVertex, g ) + Y = _.map( Y, g.getVertex, g ) + + if( typeof Z == "undefined" ){ + Z = [] + } else { + Z = _.map( Z, g.getVertex, g ) + } + if( typeof AnZ == "undefined" ){ + AnZ = g.ancestorsOf( Z ) + } else { + AnZ = _.map( AnZ, g.getVertex, g ) + } + var forward_queue = [] var backward_queue = [] var forward_visited ={} var backward_visited = {} var i, Y_ids = {}, Z_ids = {}, AnZ_ids = {}, v, vv - if( typeof AnZ == "undefined" ){ - AnZ = g.ancestorsOf( Z ) - } + for( i = 0 ; i < X.length ; i ++ ){ backward_queue.push( X[i] ) } @@ -1931,7 +1965,7 @@ var GraphAnalyzer = { }, ancestralInstrument : function( g, x, y, z, - g_bd, de_y ){ + g_bd, de_y ){ if( arguments.length < 5 ){ g_bd = GraphTransformer.backDoorGraph( g, [x], [y] ) } @@ -2110,11 +2144,11 @@ var GraphAnalyzer = { } } } - var vv = g.vertices.values() - for( var j = 0 ; j < vv.length ; j ++ ){ + var vv = g.vertices.values(), j + for( j = 0 ; j < vv.length ; j ++ ){ topological_index[vv[j].id] = 0 } - for( var j = 0 ; j < vv.length ; j ++ ){ + for( j = 0 ; j < vv.length ; j ++ ){ visit( vv[j] ) } return topological_index @@ -2131,16 +2165,16 @@ var GraphAnalyzer = { g.clearTraversalInfo() _.each( adj, function(v){ Graph.Vertex.markAsVisited(v) }) } ), function(v){ - reaches_source[v.id] = true - }) + reaches_source[v.id] = true + }) _.each( g.ancestorsOf( g.getTargets(), function(){ var adj = g.getAdjustedNodes() g.clearTraversalInfo() _.each( adj, function(v){ Graph.Vertex.markAsVisited(v) }) } ), function(v){ - reaches_target[v.id] = true - }) + reaches_target[v.id] = true + }) var vv = g.vertices.values() var bn_s = topological_index[s0.id] var bn_t = topological_index[t0.id] @@ -2552,7 +2586,7 @@ var GraphAnalyzer = { containsSemiCycle: function (g){ return GraphAnalyzer.containsCycle( GraphTransformer.contractComponents(g, GraphAnalyzer.connectedComponents(g), [Graph.Edgetype.Directed]) - ) + ) }, /* Check whether the directed edge e is stronlgy protected */ @@ -2670,7 +2704,7 @@ var GraphAnalyzer = { _.each( v.outgoingEdges, function( e ){ var vc = e.v1 === v ? e.v2 : e.v1 if (e.directed == Graph.Edgetype.Bidirected) { - trek.push( e ) + trek.push( e ) visitDown( vc, trek ) trek.pop() } @@ -2803,10 +2837,10 @@ var GraphAnalyzer = { } var primes = findPrimes(3*(MPolyHelper.variableCount + 1) + 20 ) var simulated = [ - simulateNumeric(function(){ return Math.floor((1 << 52)*Math.random()) + 1 }), - simulateNumeric(function(i){ return primes[3*i+20] }) - //simulateNumeric(function(i){ return 1 }) this would be stupid, but it works - ] + simulateNumeric(function(){ return Math.floor((1 << 52)*Math.random()) + 1 }), + simulateNumeric(function(i){ return primes[3*i+20] }) + //simulateNumeric(function(i){ return 1 }) this would be stupid, but it works + ] @@ -2879,17 +2913,17 @@ var GraphAnalyzer = { var si3 = sigma[p][q] var si4 = sigma[i][q].negate() return FASTP.make( fastp.r().mul(si2).add(fastp.p().mul(si1)), fastp.s() ? fastp.t().mul(si2).add(fastp.q().mul(si1)) : null, - fastp.r().mul(si4).add(fastp.p().mul(si3)), fastp.s() ? fastp.t().mul(si4).add(fastp.q().mul(si3)) : null, - fastp.s() ) + fastp.r().mul(si4).add(fastp.p().mul(si3)), fastp.s() ? fastp.t().mul(si4).add(fastp.q().mul(si3)) : null, + fastp.s() ) } ) ID[j] = {propagate: i, - propagatePath: "propagate" in ID[i] ? ID[i].propagatePath.concat([i]) : [i], - fastp: newfastp, - propagatedMissingCycles: "propagate" in ID[i] ? ID[i].propagatedMissingCycles : ID[i].missingCycles, - oldMissingCycles: ID[j] ? ID[j].missingCycles : null - // oldPropagatedMissingCycles: ID[j] ? ID[j].propagatedMissingCycles : null, - } - propagate(j) + propagatePath: "propagate" in ID[i] ? ID[i].propagatePath.concat([i]) : [i], + fastp: newfastp, + propagatedMissingCycles: "propagate" in ID[i] ? ID[i].propagatedMissingCycles : ID[i].missingCycles, + oldMissingCycles: ID[j] ? ID[j].missingCycles : null + // oldPropagatedMissingCycles: ID[j] ? ID[j].propagatedMissingCycles : null, + } + propagate(j) } ) } @@ -2907,9 +2941,9 @@ var GraphAnalyzer = { var p = pa[i] var q = pa[j] return [ sigma[p][q], - sigma[p][j].negate(), //lambda[p][i] - sigma[i][q].negate(), //lambda[q][j] - sigma[i][j] ] + sigma[p][j].negate(), //lambda[p][i] + sigma[i][q].negate(), //lambda[q][j] + sigma[i][j] ] } function missingCycleToQuadraticEquation(cycle){ @@ -2921,7 +2955,7 @@ var GraphAnalyzer = { var C = 2 var D = 3 var k = cycle.length - 1 - // var vars = cycle.slice(0, k) + // var vars = cycle.slice(0, k) var eqs = _.map(new Array(k), function(x,i) { return bidiEquation(cycle[i], cycle[i+1]) } ) while (k > 2) { var newk = Math.ceil(k/2) @@ -2935,7 +2969,7 @@ var GraphAnalyzer = { DET2(eqs[i][C], eqs[i+1][C], eqs[i+1][A], eqs[i][D]), DET2(eqs[i][C], eqs[i+1][D], eqs[i+1][B], eqs[i][D]) ] - // newvars[j] = vars[i]?? + // newvars[j] = vars[i]?? } if (k % 2 == 1) neweqs[newk - 1] = eqs[k - 1] k = newk @@ -2944,10 +2978,10 @@ var GraphAnalyzer = { } if (k == 2) { eqs[0] = [ DET2(eqs[0][A], eqs[1][C], eqs[1][A], eqs[0][B]), - DET2(eqs[0][A], eqs[1][D], eqs[1][A], eqs[0][D]), - DET2(eqs[0][C], eqs[1][C], eqs[1][B], eqs[0][B]), - DET2(eqs[0][C], eqs[1][D], eqs[1][B], eqs[0][D]) - ] + DET2(eqs[0][A], eqs[1][D], eqs[1][A], eqs[0][D]), + DET2(eqs[0][C], eqs[1][C], eqs[1][B], eqs[0][B]), + DET2(eqs[0][C], eqs[1][D], eqs[1][B], eqs[0][D]) + ] } var a = eqs[0][0] var b = eqs[0][1].add(eqs[0][2]) @@ -2965,7 +2999,7 @@ var GraphAnalyzer = { var minus_b = b.negate() if (isZeroSigmaPoly(ss)) return [ FASTP.makeFraction(minus_b, two_a) ] return [ FASTP.make(minus_b, MINUS_ONE, two_a, ZERO, ss), - FASTP.make(minus_b, ONE, two_a, ZERO, ss) ] + FASTP.make(minus_b, ONE, two_a, ZERO, ss) ] } function fastpMightSatisfyQuadraticEquation(fastp, abc) { @@ -2995,17 +3029,17 @@ var GraphAnalyzer = { t = t.evalToBigInt(simulated[i]) var app_aqqs_bpr_bqst_crr_ctts = (a*p*p) + (a*q*q*s) + (b*p*r) + (b*q*s*t) + (c*r*r) + (c*t*t*s) - var minus_2apq_bpt_bqr_2crt = - ((2n*a*p*q) + (b*p*t) + (b*q*r) + (2n* c*r*t)) + var minus_2apq_bpt_bqr_2crt = - (((2n)*a*p*q) + (b*p*t) + (b*q*r) + ((2n)* c*r*t)) if (app_aqqs_bpr_bqst_crr_ctts < 0 && minus_2apq_bpt_bqr_2crt > 0) return false if (app_aqqs_bpr_bqst_crr_ctts > 0 && minus_2apq_bpt_bqr_2crt < 0) return false if (app_aqqs_bpr_bqst_crr_ctts != 0 && minus_2apq_bpt_bqr_2crt == 0) return false if (app_aqqs_bpr_bqst_crr_ctts == 0 && minus_2apq_bpt_bqr_2crt != 0 && s != 0) return false - if (app_aqqs_bpr_bqst_crr_ctts**2n != (minus_2apq_bpt_bqr_2crt**2n) * s) return false + if (app_aqqs_bpr_bqst_crr_ctts ** 2n != (minus_2apq_bpt_bqr_2crt**2n) * s) return false } return true -/* Symbolic approach does not work (too slow and loses sign) + /* Symbolic approach does not work (too slow and loses sign) app_aqqs_bpr_bqst_crr_ctts = ADD(MUL(a,p,p), MUL(a,q,q,s), MUL(b,p,r), MUL(b,q,s,t), MUL(c,r, r), MUL(c,t,t,s)) var TWO = MPoly("2") apq2_bpt_bqr_2crt = ADD(MUL(TWO,a,p,q), MUL(b,p,t), MUL(b,q,r), MUL(TWO, c,r,t))*/ @@ -3296,10 +3330,17 @@ GraphLayouter.Spring.prototype = { var GraphParser = { VALIDATE_GRAPH_STRUCTURE : false, - parseDot : function( code ){ + parseDot : function( code, g ){ "use strict" + code = code.trim() + var isdot = code.trim().match( /^(digraph|graph|dag|pdag|mag|pag)(\s+\w+)?\s*\{([\s\S]*)\}$/mi ) + if( !isdot || (isdot.length <= 1) ){ + code = "dag{ " + code + "}" + } var ast = GraphDotParser.parse( code ) - var g = new Graph() + if( typeof g === "undefined" ){ + g = new Graph() + } this.parseDotStatementArray( ast.statements, g ) g.setType( ast.type ) if( ast.name ){ g.setName( ast.name ) } @@ -3639,7 +3680,7 @@ var GraphParser = { var hasarrow = firstarg.match( /(->|<->|<-)/mi ) // allow users to omit explicit "dag{ ... }" if at least one arrow is also specified if( hasarrow && hasarrow.length >= 1 ){ - return this.parseDot( "dag{"+firstarg+"}" ) + return this.parseDot( firstarg ) } else { return this.parseAdjacencyList( adjacencyListOrMatrix, vertexLabelsAndWeights ) } diff --git a/test/package-lock.json b/test/package-lock.json new file mode 100644 index 0000000..6f33f82 --- /dev/null +++ b/test/package-lock.json @@ -0,0 +1,50 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + }, + "globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" + }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, + "node-watch": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.3.tgz", + "integrity": "sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==" + }, + "qunit": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.18.0.tgz", + "integrity": "sha512-Xw/zUm5t1JY8SNErki/qtw4fCuaaOZL+bPloZU+0kto+fO8j1JV9MQWqXO4kATfhEyJohlsKZpfg1HF7GOkpXw==", + "requires": { + "commander": "7.2.0", + "node-watch": "0.7.3", + "tiny-glob": "0.2.9" + } + }, + "tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "requires": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "underscore": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", + "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==" + } + } +} diff --git a/test/package.json b/test/package.json new file mode 100644 index 0000000..49ad8c0 --- /dev/null +++ b/test/package.json @@ -0,0 +1,9 @@ +{ + "scripts": { + "test": "qunit" + }, + "dependencies": { + "qunit": "^2.18.0", + "underscore": "^1.13.2" + } +} diff --git a/test/test-graphs.js b/test/test-graphs.js index dd0f1e7..e105789 100644 --- a/test/test-graphs.js +++ b/test/test-graphs.js @@ -1,4 +1,28 @@ -var TestGraphs = { + +/** START UMD boilerplate */ +(function (root, factory) { + if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + const {GraphParser,Graph} = require('../jslib/dagitty-node.js') + const examples = require('../gui/js/example-dags.js') + module.exports = factory({GraphParser:GraphParser, Graph:Graph, examples:examples}) + } else { + // Browser globals (root is window) + root.TestGraphs = factory({GraphParser:root.GraphParser, Graph:root.Graph, examples:root.examples}); + } +}(typeof self !== 'undefined' ? self : this, function (dagitty) { + // Use b in some fashion. + let GraphParser = dagitty.GraphParser + let Graph = dagitty.Graph + let examples = dagitty.examples + + // Just return a value to define the module export. + // This example returns an object, but the module + // can return a function as the exported value. + return { +/** END UMD boilerplate */ findExample : function( s ){ for( var i = 0 ; i < examples.length ; i++ ){ @@ -178,10 +202,10 @@ small3 : function(){ small2 : function(){ var g = new Graph(); - _.each(["s","t","a","b","c","g"],function(vid){ + ["s","t","a","b","c","g"].forEach(function(vid){ g.addVertex( new Graph.Vertex({id:vid}) ); }); - _.each([ + [ ["a","b"], ["a","s"], ["b","t"], @@ -189,7 +213,7 @@ small2 : function(){ ["c","t"], ["c","b"], ["g","c"], - ["g","s"] ],function( e ){ + ["g","s"] ].forEach(function( e ){ g.addEdge( e[0], e[1] ); }); g.addAdjustedNode("b"); @@ -214,21 +238,21 @@ small1 : function(){ intermediate_adjustment_graph : function(){ var g = new Graph(); - g.setType("dag") - _.each(["X","Y","Z","I"],function(v){ + g.setType("dag"); + ["X","Y","Z","I"].forEach(function(v){ g.addVertex( new Graph.Vertex( {id:v} ) ); }); g.addSource( g.getVertex("X") ); g.addTarget( g.getVertex("Y") ); - _.each([ + [ ["X","Y"], ["X","I"], ["I","Y"], ["Z","X"], ["Z","I"] - ],function(e) { + ].forEach( function(e) { g.addEdge( e[0], e[1] ); } ); @@ -1591,3 +1615,7 @@ spirtes : GraphParser.parseGuess("pag G {"+ "}") }; + +/** START UMD boilerplate */ +})); +/** END UMD boilerplate */ diff --git a/test/test/adjustment-dags.js b/test/test/adjustment-dags.js new file mode 100644 index 0000000..b9c80bb --- /dev/null +++ b/test/test/adjustment-dags.js @@ -0,0 +1,125 @@ + +const {GraphParser,GraphAnalyzer,GraphTransformer} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") +const _ = require("underscore") + +const $p = (s) => GraphParser.parseGuess(s) +const sep_2_str = (ss) => { + var r = []; + if( ss.length == 0 ) + return ""; + _.each( ss, function(s){ + var rs = _.pluck( s, 'id').sort().join(", "); + r.push(rs); + }); + r.sort(); + return "{"+r.join("}\n{")+"}"; +} + +QUnit.module("dagitty") + +QUnit.test( "adjustment in DAGs", function( assert ) { + + assert.equal((function(){ + var g = $p("pdag{ x -> {a -- b -- c -- a } b -> y x[e] y[o] }") + return _.pluck(GraphAnalyzer.properPossibleCausalPaths(g),"id").sort().join(" ") + })(), "a b c x y" ) + + assert.equal((function(){ + var g = $p("pdag{ x -> a -- b -> y x[e] y[o] }") + return _.pluck(GraphAnalyzer.properPossibleCausalPaths(g),"id").sort().join(" ") + })(), "a b x y" ) + + assert.equal((function(){ + var g = $p("pdag{ x -> a -- b -- c -> y x[e] y[o] }") + return _.pluck(GraphAnalyzer.properPossibleCausalPaths(g),"id").sort().join(" ") + })(), "a b c x y" ) + + + assert.equal((function(){ + // our non-X-ancestor MSAS example from the UAI paper w/ causal edge + var g = $p("X1 E\nX2 E\nY O\nM1 1\nM2 1\n\nX1 Y M1\nY M2\nM1 M2\nM2 X2") + return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) + })(), "" ) + + assert.equal((function(){ + // our non-X-ancestor MSAS example from the UAI paper w/o causal edge + var g = $p("X1 E\nX2 E\nY O\nM1 1\nM2 1\n\nX1 M1\nY M2\nM1 M2\nM2 X2") + return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) + })(), "{M1, M2}" ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "Thoemmes" ) + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ) + })(), "" ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "Thoemmes" ) + g.removeAdjustedNode("s2") + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ) + })(), "{}" ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "Thoemmes" ) + return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) + })(), "{e2, s2}\n{s1, s2}" ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "mediat" ) + return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) + })(), "{I}" ) + + assert.equal((function(){ + var g = $p("dag{x[e] y[o] x<-z->y}") + return GraphAnalyzer.isAdjustmentSet(g,["z"]) + })(), true) + + assert.equal((function(){ + var g = $p("dag{x[e] y[o] x<->m<->y x->y}") + return GraphAnalyzer.isAdjustmentSet(g,["m"]) + })(), false) + + assert.equal( + sep_2_str( GraphAnalyzer.listMsasTotalEffect( TestGraphs.findExample("Polzer") ) ), + "{Age, Alcohol, Diabetes, Obesity, Psychosocial, Sex, Smoking, Sport}\n"+ + "{Age, Alcohol, Periodontitis, Psychosocial, Sex, Smoking}" ) + assert.equal((function(){ + var g = TestGraphs.m_bias_graph(); + g.addAdjustedNode("A"); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); + })(), "{A}" ) + assert.equal((function(){ + var g = TestGraphs.m_bias_graph(); + g.addAdjustedNode("M"); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); + })(), "{M, U1}\n{M, U2}" ) + assert.equal((function(){ + var g = TestGraphs.findExample( "Acid" ); + g.addAdjustedNode("x9"); + return GraphAnalyzer.violatesAdjustmentCriterion( g ) + })(), true ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "Acid" ); + g.addAdjustedNode("x16"); + return GraphAnalyzer.violatesAdjustmentCriterion( g ) + })(), true ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "Acid" ); + return GraphAnalyzer.violatesAdjustmentCriterion( g ) + })(), false ) + + assert.equal( sep_2_str(GraphAnalyzer.listMsasTotalEffect( + $p("dag { X1 [exposure]\n "+ + "X2 [exposure]\n Y1 [outcome] \n Y2 [outcome]\n "+ + "C -> Y1 \n"+ + "C -> m \n X1 -> X2 \n X1 -> Y2 -> Y1 \n X2 -> Y1 \n X1 -> m2 -> m -> X2 }"))), + "{C}\n{m, m2}") + + g = $p( "dag G { a <-> b } " ) + assert.equal( GraphTransformer.trekGraph( g ).edges.length, 4, "trek graph with <->" ) + + +}); + diff --git a/test/test/adjustment-other.js b/test/test/adjustment-other.js new file mode 100644 index 0000000..338809a --- /dev/null +++ b/test/test/adjustment-other.js @@ -0,0 +1,40 @@ + +const {GraphParser,GraphAnalyzer} = require("../../jslib/dagitty-node.js") +const _ = require("underscore") + + +const $p = (s) => GraphParser.parseGuess(s) +const sep_2_str = (ss) => { + var r = []; + if( ss.length == 0 ) + return ""; + _.each( ss, function(s){ + var rs = _.pluck( s, 'id').sort().join(", "); + r.push(rs); + }); + r.sort(); + return "{"+r.join("}\n{")+"}"; +} + +QUnit.module("dagitty") + +QUnit.test( "adjustment in other graphs", function( assert ) { + assert.equal( sep_2_str(GraphAnalyzer.listMsasTotalEffect( + $p("pdag { X [exposure]\n"+ + "Y [outcome]\n"+ + "X -> W -> Y ; F -> W -- Z ; X <- F -> Z }") + ) ), + "{F}" ) + + assert.equal( sep_2_str(GraphAnalyzer.listMsasTotalEffect( + $p("mag { X [e] Y[o] X->Y }") + ) ), "" ) + + assert.equal( sep_2_str(GraphAnalyzer.listMsasTotalEffect( + $p("mag { X [e] Y[o] I->X->Y }") + ) ), "{}" ) + + assert.equal( GraphAnalyzer.listMsasTotalEffect($p($p("pag{i<-@x->y x[e] y[o]}").toString()) ).length, + 0 ) + +}); diff --git a/test/test/ancestry.js b/test/test/ancestry.js new file mode 100644 index 0000000..f97ecf4 --- /dev/null +++ b/test/test/ancestry.js @@ -0,0 +1,29 @@ + +const { + Graph, + GraphParser, + GraphTransformer, + GraphSerializer +} = require("../../jslib/dagitty-node.js") + +const _ = require("underscore") + +const TestGraphs = require("../test-graphs.js") + +QUnit.module('dagitty') + +QUnit.test( "ancestry", function( assert ) { + var shrier = TestGraphs.findExample("Shrier") + assert.equal( + (_.pluck(shrier.ancestorsOf( + shrier.getVertex(["PreGameProprioception"])),"id")).sort().join(",") + , "Coach,FitnessLevel,Genetics,PreGameProprioception" ) + + assert.equal((function(){ + var g = GraphParser.parseGuess( + "digraph G {a -- b }" ) + return g.getVertex("a").getNeighbours().length + })(), 1 ) +}) + + diff --git a/test/test/biasing-paths.js b/test/test/biasing-paths.js new file mode 100644 index 0000000..e62ddcc --- /dev/null +++ b/test/test/biasing-paths.js @@ -0,0 +1,70 @@ +const { + GraphParser,GraphTransformer,GraphSerializer +} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") +const _ = require("underscore") + +const $p = (s) => GraphParser.parseGuess(s) +const $es = (g) => GraphSerializer.toDotEdgeStatements(g) + + +QUnit.module('dagitty') + +QUnit.test( "biasing paths in DAGs (allowing <->)", function( assert ) { + assert.equal((function(){ + var g = TestGraphs.confounding_triangle_with_irrelevant_nodes(); + var vv = []; + _.each(GraphTransformer.activeBiasGraph(g).vertices.values(),function( v ){ + vv.push( v.id ); + }) + vv.sort(); + return vv.join(","); + })(),"A,B,M" ) +assert.equal((function(){ + var g = $p( "digraph G { x <-> y } " ) + return $es( + GraphTransformer.activeBiasGraph(g,g.getVertex(["x"]),g.getVertex(["y"]))) +})(), "x <-> y" ) + +assert.equal((function(){ + var g = $p( "digraph G { x <- m <-> y } " ) + g.addSource("x") + g.addTarget("y") + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "m -> x\nm <-> y" ) + +assert.equal((function(){ + var g = $p( "digraph G { x -> m <-> y } " ) + g.addSource("x") + g.addTarget("y") + g.addAdjustedNode("m") + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "m <-> y\nx -> m" ) + +assert.equal((function(){ + var g = $p( "digraph G { m <-> y \n x -> m -> y } " ) + g.addSource("x") + g.addTarget("y") + g.addAdjustedNode("m") + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "m <-> y\nx -> m" ) + +assert.equal((function(){ + var g = $p( "digraph G { m <-> y \n x -> m -> y } " ) + g.addSource("x") + g.addTarget("y") + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "" ) + +assert.equal((function(){ + var g = $p( "digraph G { M \n "+ + "X [exposure] \n "+ + "Y [outcome] \n "+ + "M <-> Y [pos=\"0.645,-0.279\"] \n "+ + "X -> Y \n "+ + "X -> M -> Y } " ) + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "" ) + +}); + diff --git a/test/test/dseparation.js b/test/test/dseparation.js new file mode 100644 index 0000000..406217e --- /dev/null +++ b/test/test/dseparation.js @@ -0,0 +1,118 @@ + +const {Graph,GraphAnalyzer} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") + + +const $p = (s) => new Graph(s) + +QUnit.module("dagitty") +QUnit.test( "dseparation", function( assert ) { + assert.equal((function(){ + var g = $p( "digraph G { x <-> m -> y }" ) + return GraphAnalyzer.dConnected( g, ["x"], ["y"], + [] ) + })(), true ) + + assert.equal((function(){ + var g = $p( "digraph G { x <-> m -> y }" ) + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + [g.getVertex("m")] ) + })(), false ) + + assert.equal((function(){ + var g = $p( "digraph G { x <-> m <-> b <-> y }" ) + return GraphAnalyzer.dConnected( g, ["x"], [g.getVertex("y")], + [g.getVertex("m")] ) + })(), false ) + + assert.equal((function(){ + var g = $p( "digraph G { x <-> m <-> b <-> y }" ) + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + [g.getVertex("m"),g.getVertex("b")] ) + })(), true ) + + assert.equal((function(){ + var g = $p( "digraph G { x -> m -> y }" ) + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], ["y"], + [] ) + })(), true ) + + assert.equal((function(){ + var g = $p( "digraph G { x -> m -> y }" ) + return !GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + [g.getVertex("m")] ) + })(), true ) + + assert.equal((function(){ + var g = $p( "digraph G { x -> m -> y }" ) + return !GraphAnalyzer.dConnected( g, ["x"], ["y"], [] ) + })(), false ) + + assert.equal((function(){ + var g = $p("dag{R->S->T<-U}") + g = GraphAnalyzer.listPaths( g, false, 1, [g.getVertex("R")], [g.getVertex("U")] )[0] + return GraphAnalyzer.dConnected( g, [g.getVertex("R")], [g.getVertex("U")], + g.getVertex(["T"]) ) + })(), true ) + + assert.equal((function(){ + var g = $p("dag{ a->x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, [g.getVertex("a")], [g.getVertex("y")], + g.getVertex(["m"]) ) + })(), true ) + + assert.equal((function(){ + var g = $p("dag{ a->x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, g.getVertex(["x"]), [g.getVertex("y")], + g.getVertex(["m"]), g.getVertices(["x"]) ) + })(), true ) + + assert.equal((function(){ + var g = $p("dag{ x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + [g.getVertex("m")] ) + })(), true ) + + assert.equal((function(){ + var g = $p("dag{ x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + g.getVertex(["p"]), g.getVertex(["m","p"]) ) + })(), true ) + + assert.equal((function(){ + var g = $p("dag{ x[e] y[o] x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + g.getVertex([]), [] ) + })(), false ) + + assert.equal((function(){ + var g = $p("dag{ x[e] y[o] x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + g.getVertex([]), g.getVertex(["m","p"]) ) + })(), true ) + + assert.equal((function(){ + var g = $p("dag{ x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + [g.getVertex("m")] ) + })(), true ) + + assert.equal((function(){ + var g = $p("dag{ x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + [g.getVertex("p")] ) + })(), true ) + + assert.equal((function(){ + var g = $p("dag{ x->m<-y m->p }") + return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], + [] ) + })(), false ) + + assert.equal((function(){ + var g = TestGraphs.findExample("onfound") + return GraphAnalyzer.dConnected( g, ["E"], ["B"], + ["Z"] ) + })(), true ) +}); + diff --git a/test/test/graph-analysis.js b/test/test/graph-analysis.js new file mode 100644 index 0000000..f85c80a --- /dev/null +++ b/test/test/graph-analysis.js @@ -0,0 +1,80 @@ + +const { + GraphAnalyzer, + GraphParser +} = require("../../jslib/dagitty-node.js") +const _ = require("underscore") +const TestGraphs = require("../test-graphs.js") + + +const $p = (s) => GraphParser.parseGuess(s) + +QUnit.test( "graph analysis", function( assert ) { + var g = $p("dag{x->y}") + assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) + g = $p("mag{x->y}") + assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),false) + g = $p("mag{z<->x->y}") + assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) + g = $p("mag{z->x->y}") + assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) + g = $p("mag{z--x->y}") + assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),false) + g = $p("mag{c<->a {a<->b<->x}->y}") + assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) + g = $p("mag{c->a {a<->b<->x}->y}") + assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) + g = $p("mag{c->{a b x}->y}") + assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) + + + g = $p("dag{x->y}") + assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("x","y")),false) + g = $p("dag{x->y<-z}") + assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("x","y")),true) + g = $p("dag{x->y<-z->x}") + assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("x","y")),false) + g = $p("dag{{a b}->x->y}") + assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("x","y")),true) + + g = $p("dag{x<-z3<-z1->x}") + assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("z1","x")),true) + assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("z3","x")),false) + assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("z1","z3")),false) + + assert.equal( + GraphAnalyzer.containsCycle( TestGraphs.cyclic_graph() ), "A→B→C→A" ) + assert.equal( + $p("x E\ny O\nz\na\n\nx y a z\na y\nz y").countPaths(),3) + assert.equal((function(){ + // note, this test is overly restrictive as only one solution is + // considered legal + var g = TestGraphs.extended_confounding_triangle(); + var to = GraphAnalyzer.topologicalOrdering(g) + var vids = _.pluck(g.vertices.values(),"id") + vids.sort( function(a,b){ return to[a] < to[b] ? -1 : 1 } ) + return vids.toString(); + })(),"E,D,C,A,B") + + var chordalGraphs = [ + "graph { t -- x \n x -- y \n y -- t }", + "graph { A -- E \n A -- Z \n B -- D \n B -- Z \n D -- Z \n E -- Z \n }", + "graph { A -- B \n A -- E \n A -- Z \n B -- D \n B -- Z \n D -- Z \n E -- Z \n }", + "graph { A -- B \n A -- E \n A -- Z \n B -- D \n B -- E \n B -- Z \n D -- Z \n E -- Z }" + ]; + _.each(chordalGraphs, function(g) { + assert.equal(GraphAnalyzer.isChordal($p(g)), true); + }); + assert.equal(GraphAnalyzer.isChordal(TestGraphs.K5), true); + + var notChordalGraphs = [ + "graph {a -- b \n a -- x \n b -- y \n x -- y}", + "graph {A -- E \n A -- Z \n B -- Z \n B -- D \n D -- E}", + "graph {A -- E \n B -- D \n B -- Z \n B -- A \n D -- Z \n E -- Z }" + ]; + _.each(notChordalGraphs, function(g) { + assert.equal(GraphAnalyzer.isChordal($p(g)), false); + }); +}); + + diff --git a/test/test/graph-transformations.js b/test/test/graph-transformations.js new file mode 100644 index 0000000..3d153bf --- /dev/null +++ b/test/test/graph-transformations.js @@ -0,0 +1,127 @@ + +const {Graph,GraphAnalyzer,GraphParser,GraphSerializer,GraphTransformer} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") +const _ = require("underscore") + +const $p = (s) => GraphParser.parseGuess(s) +const $es = (g) => GraphSerializer.toDotEdgeStatements(g) + + +QUnit.test( "graph transformations", function( assert ) { + assert.equal( + $es(GraphTransformer.lineDigraph( + $p( "dag{a<-b->c}" ))), + "") + assert.equal( + $es(GraphTransformer.lineDigraph( + $p( "dag{a->b->c}" ))), + "ab -> bc") + + assert.equal( + $es(GraphTransformer.lineDigraph( + $p( "dag{a->b [label=x] b->c [label=y]}" ))), + "x -> y") + + assert.equal( + GraphTransformer.ancestorGraph( TestGraphs.small1() ).oldToString(), + "A 1\nS E\nT O\n\nA S\nS T" ) + assert.equal( + GraphTransformer.moralGraph( + GraphTransformer.ancestorGraph(TestGraphs.small1()) ).oldToString() + , "A 1\nS E\nT O\n\nA S\nS A T\nT S" ) + assert.equal((function(){ + var g = TestGraphs.confounding_triangle_with_irrelevant_nodes(); + var vv = []; + _.each(GraphTransformer.ancestorGraph(g).vertices.values(),function( v ){ + vv.push( v.id ); + } ); + vv.sort(); + return vv.join(","); + })(), "A,B,M,U1,U2" ) + assert.equal((function(){ + var g = TestGraphs.m_bias_graph(); + g.addAdjustedNode( "M" ); + return GraphTransformer.moralGraph( + GraphTransformer.ancestorGraph(GraphTransformer.backDoorGraph(g)) ).oldToString() + })(), "A E\nB O\nM A\nU1 1\nU2 1\n\n"+ + "A U1\nB U2\nM U1 U2\nU1 A M U2\nU2 B M U1" ) + + assert.equal((function(){ + return GraphTransformer.moralGraph( + GraphTransformer.ancestorGraph( + GraphTransformer.backDoorGraph(TestGraphs.m_bias_graph())) ).oldToString() + })(), "A E\nB O\nU1 1\nU2 1\n\nA U1\nB U2\nU1 A\nU2 B" ) + + assert.equal((function(){ + var g = $p("dag{x<-z3<-z1->x}") + return GraphTransformer.markovEquivalentDags(g).length + })(),6,"markov equiv") + + var transformations = [ + GraphTransformer.pagToPdag, + "pag{x@-@y@->z}","pdag{x--y->z}", + + GraphTransformer.dagToCpdag, + "dag{x->y}", "pdag { x -- y }", + "dag{x->y<-z}", "pdag { x -> y <- z }", + "dag{x->y<-z->x}", "pdag { x -- y -- z -- x }", + "dag{z1->{x z3} z2->{z3 y} z3->{x y} x->w->y}", + "pdag {x <- z1 z1->z3 z2->z3 y<-z2 x<-z3 z3->y x->w->y}", + + GraphTransformer.cgToRcg, + "pdag { k -- n; l -- m; n -- t; t -- y; x -> y; }", + "pdag { k;l;m;n;t;x;y; l -- m; n -> k; t -> n; x -> y; y -> t }", + "pdag { k -- n; l -- m; l -> n; l -> t; l -> y; n -- t; t -- y; x -> y; }", + "pdag { k;l;m;n;t;x;y; l -- m; l -> n; l -> t; l -> y; n -> k; t -> n; x -> y; y -> t }", + "pdag {a -> b -- c <- d }", + null, + "digraph { a -> b; b -- c; c <- d; b -- d }", + "digraph { a -> b; b -> c; b -> d; d -> c }", + + function (g){ return GraphTransformer.contractComponents(g, + GraphAnalyzer.connectedComponents(g), [Graph.Edgetype.Directed])}, + "pdag { k -- n l -- m n -- t t -- y x -> y }", + 'pdag { "l,m" ; x -> "k,n,t,y" }', + "digraph { a -- b; b -> c; c -- a }", + 'digraph { "a,b,c" -> "a,b,c" }', + + GraphTransformer.transitiveClosure, + "dag G { x -> y -> z }", + "dag G { x -> y -> z <- x }", + + GraphTransformer.transitiveReduction, + "dag G { x -> y -> z <- x }", + "dag G { x -> y -> z }", + + GraphTransformer.dag2DependencyGraph, + "dag G { x -> y -> z <- x }", + "graph G { x -- y -- z -- x }", + "dag G { x -> y -> z }", + "graph G { x -- y -- z -- x }", + "dag G { x -> y <- z }", + "graph G { x -- y -- z }", + + GraphTransformer.dagToMag, + "dag { v1 [latent] z <- v1 -> v2 -> y x -> v1}", + "mag { x -> z <-> v2 -> y x -> v2 }", + "dag { l1 [latent] l2 [latent] a -> x l1 -> x l1 -> z l2 -> y l2 -> z x -> y z -> x z -> y}", + "mag { a -> x a -> y x -> y z -> x z -> y } ", + + ]; + var i = 0; var transfunc; + while (i < transformations.length) { + if (typeof transformations[i] === "function") { + transfunc = transformations[i]; + i ++; + } + var gin = transformations[i]; i++; + if (typeof gin === "string") gin = $p(gin); + var gout = transfunc(gin); + var gref = transformations[i]; i++; + if( gref != null ){ gref = $p(gref) } + assert.equal(GraphAnalyzer.equals(gout, gref),true); + } +}); + + + diff --git a/test/test/graph-types.js b/test/test/graph-types.js new file mode 100644 index 0000000..32f1604 --- /dev/null +++ b/test/test/graph-types.js @@ -0,0 +1,47 @@ + +const {Graph, GraphTransformer} = require("../../jslib/dagitty-node.js") +const _ = require("underscore") + +QUnit.module( "dagitty" ) +QUnit.test( "graph types", function( assert ) { + var graphs = { + graph : new Graph( "graph { x -- y -- z }" ), + dag : new Graph( "dag { x -> y -> z }" ), + pdag : new Graph( "pdag { x -- y -> z }" ), + mag : new Graph( "mag { x <-> y -> z }" ) + }; + + _.each( Object.keys(graphs), function(t){ + assert.equal( GraphTransformer.inducedSubgraph(graphs[t], + graphs[t].getVertex(["x","y"])).getType(), t ) + }); + + _.each( Object.keys(graphs), function(t){ + assert.equal( GraphTransformer.edgeInducedSubgraph(graphs[t], + graphs[t].edges).getType(), t ) + }); + + _.each( Object.keys(graphs), function(t){ + assert.equal( GraphTransformer.ancestorGraph(graphs[t], + graphs[t].getVertex(["x","y"])).getType(), t ) + }); + + _.each( ["backDoorGraph","indirectGraph","activeBiasGraph"], function(f){ + _.each( Object.keys(graphs), function(t){ + assert.equal( GraphTransformer[f](graphs[t], + graphs[t].getVertex(["x"]),graphs[t].getVertex(["z"])).getType(), t ) + }) + }); + + _.each( Object.keys(graphs), function(t){ + assert.equal( GraphTransformer.canonicalDag(graphs[t]).g.getType(), "dag" ) + }); + + _.each( ["moralGraph","skeleton"], function(f){ + _.each( Object.keys(graphs), function(t){ + assert.equal( GraphTransformer[f](graphs[t]).getType(), "graph" ) + }); + }); + +}); + diff --git a/test/test/graph-validation.js b/test/test/graph-validation.js new file mode 100644 index 0000000..fdc0614 --- /dev/null +++ b/test/test/graph-validation.js @@ -0,0 +1,29 @@ + +const {Graph,GraphAnalyzer,GraphParser} = require("../../jslib/dagitty-node.js") + + +QUnit.module( "dagitty" ) +QUnit.test( "graph validation", function( assert ) { + + GraphParser.VALIDATE_GRAPH_STRUCTURE = false + + assert.true( GraphAnalyzer.validate( new Graph( + "dag { x -> y -> z }" + ))) + + assert.false( GraphAnalyzer.validate( new Graph( "dag{ x -> y -> z -> x } " ) ) ) + + assert.true( GraphAnalyzer.validate( new Graph( + "pdag { x -> y -> z }" + ))) + + assert.false( GraphAnalyzer.validate( new Graph( + "dag { x -- y -> z -> x }" + ))) + + assert.true( GraphAnalyzer.validate( new Graph( + "pdag { x -- y -> z }" + ))) + + GraphParser.VALIDATE_GRAPH_STRUCTURE = true; +}); diff --git a/test/test/instrumental-variables.js b/test/test/instrumental-variables.js new file mode 100644 index 0000000..99206ff --- /dev/null +++ b/test/test/instrumental-variables.js @@ -0,0 +1,64 @@ +const {GraphAnalyzer,GraphParser} = require("../../jslib/dagitty-node.js") +const _ = require("underscore") +const $p = (s) => GraphParser.parseGuess(s) +const iv_2_str = ( ivs ) => { + var r = [] + _(ivs).each( function( i ){ + if( i[1].length > 0 ){ + r.push( i[0].id+" | "+_(i[1]).pluck('id').sort().join(", ") ) + } else { + r.push( i[0].id ) + } + } ) + return r.sort().join("\n") +} +QUnit.module("dagitty") + +QUnit.test( "instrumental variables", function( assert ) { + assert.equal((function(){ + var g = $p( "digraph G { u1 [latent] \n u2 [latent] \n"+ + "u2 -> d -> a -> z \n a -> c -> b -> z \n "+ + "u2 -> y \n b -> u1 -> y \n "+ + "\n }" ) + return _.pluck(GraphAnalyzer.closeSeparator( + g, g.getVertex("y"), g.getVertex("z") + ),"id").join(",") + })(), "b,d" ) + + assert.equal((function(){ + var g = $p( "digraph G { u [latent] \n x [exposure] \n y [outcome] \n"+ + " w -> z -> x -> y \n w -> u -> x \n u -> y \n }" ) + return iv_2_str( GraphAnalyzer.conditionalInstruments( g ) ) + })(), "z | w" ) + + assert.equal((function(){ + var g = $p( "digraph G { u [latent] \n x [exposure] \n y [outcome] \n"+ + " z -> x -> y \n u -> x \n u -> y \n }" ) + return iv_2_str( GraphAnalyzer.conditionalInstruments( g ) ) + })(), "z" ) + + assert.equal((function(){ + var g = $p( "digraph G { u \n w -> z -> x -> y \n w -> u -> x \n u -> y }" ) + return _(GraphAnalyzer.ancestralInstrument( g, g.getVertex("x"), g.getVertex("y"), + g.getVertex("z") )).pluck("id").join(",") + })(), "u" ) + + assert.equal((function(){ + var g = $p( "dag{ u \n z -> x -> y \n u -> x \n u -> y \n }" ) + return ""+_(GraphAnalyzer.conditionalInstruments( g, + g.getVertex("x"), g.getVertex("y"))[0][1]).pluck("id").join(",") + })(), "" ) + + assert.equal((function(){ + var g = $p( "digraph G { u [latent] \n w -> z -> x -> y \n w -> u -> x \n u -> y }" ) + return _(GraphAnalyzer.ancestralInstrument( g, g.getVertex("x"), g.getVertex("y"), + g.getVertex("z") )).pluck("id").join(",") + })(), "w" ) + + assert.equal((function(){ + var g = $p( "digraph G { u [latent] \n z -> x -> y \n u -> x \n u -> y \n }" ) + return ""+_(GraphAnalyzer.ancestralInstrument( g, g.getVertex("x"), g.getVertex("y"), + g.getVertex("z") )).pluck("id").join(",") + })(), "" ) +}); + diff --git a/test/test/manipulation.js b/test/test/manipulation.js new file mode 100644 index 0000000..2875b52 --- /dev/null +++ b/test/test/manipulation.js @@ -0,0 +1,55 @@ + +const { + Graph, + GraphParser, + GraphTransformer, + GraphSerializer +} = require("../../jslib/dagitty-node.js") + +const $p = (s) => GraphParser.parseGuess(s) +const $es = (g) => GraphSerializer.toDotEdgeStatements(g) + +QUnit.test( "graph manipulation", function( assert ) { + + var g = $p( "dag G { x <-> x }" ) + assert.equal( g.areAdjacent("x","x"), true, "self loop" ) + + g = $p( "map <-> pop" ) + g.changeEdge( g.getEdge("map","pop",Graph.Edgetype.Bidirected), Graph.Edgetype.Directed ) + assert.equal( $es( g ), "map -> pop", "reserved words" ) + + g = $p( "dag G { x <-> y }" ) + assert.equal( g.areAdjacent("x","y"), true ) + assert.equal( g.areAdjacent("y","x"), true ) + g.changeEdge( g.getEdge("x","y",Graph.Edgetype.Bidirected), Graph.Edgetype.Directed ) + assert.equal( $es( g ), "x -> y" ) + assert.equal( g.areAdjacent("x","y"), true ) + assert.equal( g.areAdjacent("y","x"), true ) + + g = $p( "digraph G { x -> y }" ) + assert.equal( g.areAdjacent("x","y"), true ) + g.changeEdge( g.getEdge("x","y"), Graph.Edgetype.Undirected ) + assert.equal( $es( g ), "x -- y" ) + assert.equal( g.areAdjacent("x","y"), true ) + + g = $p( "digraph G { x -> y }" ) + assert.equal( g.areAdjacent("x","y"), true ) + g.reverseEdge( g.getEdge("x","y"), Graph.Edgetype.Undirected ) + assert.equal( $es( g ), "y -> x" ) + assert.equal( g.areAdjacent("x","y"), true ) + + g = $p( "digraph G { x -- y }" ) + assert.equal( g.areAdjacent("x","y"), true ) + g.changeEdge( g.getEdge("y","x",Graph.Edgetype.Undirected), Graph.Edgetype.Directed ) + assert.equal( $es( g ), "x -> y" ) + assert.equal( g.areAdjacent("x","y"), true ) + + g = $p( "digraph G { x -- y }" ) + g.changeEdge( g.getEdge("y","x",Graph.Edgetype.Undirected), Graph.Edgetype.Directed, "y" ) + assert.equal( $es( g ), "y -> x", "edge change" ) + + g = $p( "digraph G { y <- x <-> m -- y }") + g.deleteVertex("m") + assert.equal( $es( g ), "x -> y", "vertex deletion" ) +} ) + diff --git a/test/test/misc.js b/test/test/misc.js new file mode 100644 index 0000000..29627d9 --- /dev/null +++ b/test/test/misc.js @@ -0,0 +1,729 @@ + + +const {Graph,GraphParser,GraphAnalyzer,GraphTransformer,GraphSerializer} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") +const _ = require("underscore") +const $es = (g) => GraphSerializer.toDotEdgeStatements(g) +const $p = (s) => GraphParser.parseGuess(s) + +function sep_2_str( ss ){ + var r = []; + if( ss.length == 0 ) + return ""; + _.each( ss, function(s){ + var rs = _.pluck( s, 'id').sort().join(", "); + r.push(rs); + }); + r.sort(); + return "{"+r.join("}\n{")+"}"; +} +function imp_2_str( imp ){ + var r = [],j,rr; + _.each( imp, function( i ){ + for( j = 0 ; j < i[2].length ; j ++ ){ + rr = i[0]+" _||_ "+i[1]; + if( i[2][j].length > 0 ){ + rr += " | "+_.pluck( i[2][j], 'id').sort().join(", "); + } + r.push(rr) + } + } ); + return r.join("\n"); +} + +QUnit.module("dagitty") + + +QUnit.test( "uncategorized tests", function( assert ) { + +assert.equal((function(){ + var g = TestGraphs.small3(); + g.addSource("S"); + g.addTarget("T"); + g.addAdjustedNode("p"); + var abg = GraphTransformer.activeBiasGraph(g); + var gbd = GraphTransformer.backDoorGraph(abg); + var gbdan = GraphTransformer.ancestorGraph(gbd); + var gam = GraphTransformer.moralGraph( gbdan ) + // the undirected edges from the active bias graph graph should + // not yield an edge x -- y in the moral graph, hence {p} again + // becomes a valid separator + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( abg ) ); +})(), "{p, x}\n{p, z}" ) + +assert.equal((function(){ + var g = TestGraphs.big1(); + g.addAdjustedNode("y"); + var g_bias = GraphTransformer.activeBiasGraph( g ) + g = GraphTransformer.edgeInducedSubgraph( g, g_bias.edges ) + g = GraphTransformer.moralGraph( g ) + g.deleteVertex(g.getVertex("x")) + return sep_2_str( GraphAnalyzer.listMinimalSeparators( g ) ) +})(), "{a, h}\n{e, h}\n{f, h}\n{h, n}" ) + +assert.equal((function(){ + var g = TestGraphs.small2(); + g = GraphTransformer.activeBiasGraph(g); + return g.toAdjacencyList(); +})(), "a b s\nc b t\ng c s" ) + +assert.equal((function(){ + var g = TestGraphs.findExample("Schipf"); + g.addAdjustedNode("WC"); + g.addAdjustedNode("U"); + g = GraphTransformer.activeBiasGraph(g); + return $es(g); +})(), "A -> PA\nA -> S\nA -> TT\nA -> WC\nPA -> T2DM\nPA -> WC\nS -> T2DM\nS -> TT" ) + +assert.equal((function(){ + var g = TestGraphs.extended_confounding_triangle() + var gbias = GraphTransformer.activeBiasGraph(g) + var gmor = GraphTransformer.moralGraph( gbias ) + var gsep = GraphAnalyzer.listMinimalSeparators( + gmor, [g.getVertex("D")], [] ) + return sep_2_str( gsep ) +})(), "{C, D}" ) + +assert.equal((function(){ + var g = TestGraphs.findExample("Acid"); + return sep_2_str( + GraphAnalyzer.listMinimalSeparators( + GraphTransformer.moralGraph( GraphTransformer.activeBiasGraph(g) ), [], g.descendantsOf(g.getSources()) ) ); +})(), "{x1}\n{x4}" ) + +assert.equal((function(){ + var g = TestGraphs.intermediate_adjustment_graph(); + return sep_2_str( + GraphAnalyzer.listMinimalSeparators( + GraphTransformer.moralGraph(GraphTransformer.activeBiasGraph(g)), [], [g.getVertex('I')] ) ); +})(), "{Z}" ) + +assert.equal((function(){ + var g = TestGraphs.intermediate_adjustment_graph(); + g.getVertex('I').latent = true; + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "{Z}" ) + +assert.equal((function(){ + var g = TestGraphs.small_mixed(); + var cc = GraphAnalyzer.connectedComponents(g); + var r = ""; + _.each(cc, function(c) { + r += ("["+_.pluck(c,'id').sort().join(",")+"] "); + } ); + return r; +})(), "[a,b,c] [d,e,f] " ) + +assert.equal((function(){ + // This test verifies that the below methods have + // no side effects (which they had in an earlier, buggy + // version of the code) + var g = TestGraphs.extended_confounding_triangle(); + GraphTransformer.ancestorGraph(g); + GraphTransformer.ancestorGraph(g); + GraphTransformer.activeBiasGraph(g); + GraphTransformer.activeBiasGraph(g); + return g.oldToString(); +})(), "A E\nB O\nC 1\nD 1\nE 1\n\nA B\nC A B\nD A C\nE B C" ) + +assert.equal((function(){ + var g = GraphTransformer.moralGraph( $p( "x 1\ny 1\nm 1\na 1\nb 1\n\nm x\nm y\na m x\nb m y" ) ) + return _.pluck(GraphAnalyzer.connectedComponentAvoiding( g, + [g.getVertex("x")], [g.getVertex("m"), g.getVertex("b")] ),'id') + .sort().join(","); +})(), "a,x" ) + +assert.equal((function(){ + GraphParser.VALIDATE_GRAPH_STRUCTURE = false; + var g = $p( "xobs 1 @0.350,0.000\n"+ +"y 1 @0.562,0.000\n"+ +"t 1 @0.351,-0.017\n"+ +"\n"+ +"xobs y\n"+ +"y t\n"+ +"t xobs" ); + GraphParser.VALIDATE_GRAPH_STRUCTURE = true; + GraphAnalyzer.containsCycle( g ); + return GraphAnalyzer.containsCycle( g ); +})(), "xobs→y→t→xobs" ) + +assert.equal((function(){ + var g = $p( "xobs E @0.350,0.000\n"+ +"y O @0.562,0.000\n"+ +"t 1 @0.351,-0.017\n"+ +"u1 1 @0.476,-0.013\n"+ +"u2 1 @0.175,-0.017\n"+ +"\n"+ +"t xobs\n"+ +"u1 t y\n"+ +"u2 t xobs\n"+ +"xobs y" ); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "{t, u2}\n{u1}" ) + +assert.equal((function(){ + var g = TestGraphs.small5() + g.addAdjustedNode("A") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "T A\nU A I" ) + +assert.equal((function(){ + var g = TestGraphs.small5(); + return GraphTransformer.causalFlowGraph(g).toAdjacencyList(); +})(), "A I\nT A" ) + +assert.equal((function(){ + var g = TestGraphs.small5(); + g.addAdjustedNode("A"); + return GraphTransformer.causalFlowGraph(g).toAdjacencyList(); +})(), "" ) + +assert.equal((function(){ + var g = TestGraphs.small4(); + g.addAdjustedNode("A"); + var abg = GraphTransformer.activeBiasGraph(g); + return abg.oldToString() +})(), "I O\nT E\n\n" ) + +assert.equal((function(){; + var g = TestGraphs.small3(); + g.addAdjustedNode("p"); + g.addSource("S"); + g.addTarget("T"); + var abg = GraphTransformer.activeBiasGraph(g); + g.deleteVertex( "p" ) + return GraphTransformer.edgeInducedSubgraph(g,abg.edges).toAdjacencyList() +})(), "x S\nz T" ) + +assert.equal((function(){; + var g = TestGraphs.small3(); + g.addSource("S"); + g.addTarget("T"); + g.addAdjustedNode("p"); + // this should yield the same result as listing the MSAS of g + // the vertex p should not be listed as contained in the separators + // because it is not listed as compulsory in the call to "listSeparators()" + // (see the api of the function there) + var g_bias = GraphTransformer.activeBiasGraph( g ) + var g_can = GraphTransformer.canonicalDag( g_bias ) + g = GraphTransformer.moralGraph( g_can.g ) + return sep_2_str( GraphAnalyzer.listMinimalSeparators( g, [], + g.getAdjustedNodes() ) ) +})(), "{x}\n{z}" ) + +assert.equal((function(){ + var g = TestGraphs.commentator1(); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "" ) + +assert.equal((function(){ + var g = $p("S E @0.395,0.046\n"+ +"T O @0.715,0.039\n"+ +"h 1 @0.544,0.017\n"+ +"i 1 @0.544,0.017\n"+ +"x 1 @0.424,0.072\n"+ +"z 1 @0.610,0.071\n"+ +"\n"+ +"S T\n"+ +"h S\n"+ +"i h T\n"+ +"x S z\n"+ +"z T\n"); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "{h, x}\n{h, z}\n{i, x}\n{i, z}" ) + +assert.equal((function(){ + var g = TestGraphs.big1(); + g.addAdjustedNode("y"); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "{a, h, x, y}\n{a, h, y, z}\n{e, h, x, y}\n{e, h, y, z}\n{f, h, x, y}\n{f, h, y, z}\n{h, n, x, y}\n{h, n, y, z}" ) + +assert.equal((function(){ + var g = TestGraphs.findExample("Shrier"); + var must = [g.getVertex("FitnessLevel")]; + var must_not = [g.getVertex("Genetics"), + g.getVertex("ConnectiveTissueDisorder"), + g.getVertex("IntraGameProprioception")]; + + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g, must, must_not ) ); +})(), "{Coach, FitnessLevel}\n{FitnessLevel, NeuromuscularFatigue, TissueWeakness}\n{FitnessLevel, TeamMotivation}" ) + +assert.equal((function(){ + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( TestGraphs.very_large_dag() ) ); +})(), "{Allergenexposition, Antibiotika, Begleiterkrankungen, BetreuungKind, Darmflora, Erregerexposition, Geschwister, Hausstaub, Haustiere, Infektionen, RauchenAnderer, RauchenMutter, Stillen}\n{Allergenexposition, Antibiotika, Begleiterkrankungen, Darmflora, Erregerexposition, Geburtsmodus, Hausstaub, Haustiere, Infektionen, RauchenAnderer, RauchenMutter, Stillen}\n{Allergenexposition, Begleiterkrankungen, Darmflora, Erregerexposition, Impfungen, Infektionen, RauchenAnderer, RauchenMutter}" ) + +assert.equal((function(){ + var g = GraphTransformer.moralGraph( $p( "x E\ny O\nm\na\nb\n\nm x\nm y\na m x\nb m y" ) ) + return sep_2_str( GraphAnalyzer.listMinimalSeparators(g) ); +})(), "{a, m}\n{b, m}" ) + +assert.equal((function(){ + // the function "neighboursOf" should, also when called on a vertex set, + // not return any vertices from those sets as neighbours of the set itself + var g = GraphTransformer.moralGraph( $p( "x\ny\nm\na\nb\n\nm x\nm y\na m x\nb m y" ) ) + return _.pluck(g.neighboursOf( [g.getVertex("m"), g.getVertex("b")] ),'id') + .sort().join(","); +})(), "a,x,y" ) + +assert.equal((function(){ + var g = $p("E E @-1.897,0.342\n"+ + "D O @-0.067,0.302\n"+ + "g A @-0.889,1.191\n"+ + "\n"+ + "D g\n"+ + "E D"); + return GraphAnalyzer.violatesAdjustmentCriterion( g ) +})(), true ) + +assert.equal((function(){ + var g = $p("x E @0.083,-0.044\n"+ + "y O @0.571,-0.043\n"+ + "i1 1 @0.331,-0.037\n"+ + "i2 1 @0.328,-0.030\n"+ + "y2 A @0.333,-0.054\n"+ + "y3 1 @0.334,-0.047\n"+ + "\n"+ + "i1 y\n"+ + "i2 y\n"+ + "x y2 i2 i1 y\n"+ + "y3 y x"); + return _.pluck(GraphAnalyzer.intermediates(g),'id').join(","); +})(), "i1,i2" ) + +assert.equal((function(){ + var g = $p("x 1 @0.264,-0.027\n"+ + "y 1 @0.537,-0.015\n"+ + "y2 A @0.216,-0.015\n"+ + "\n"+ + "x y2 y"); + return _.pluck(GraphAnalyzer.intermediates(g),'id').join(","); +})(), "" ) + +assert.equal((function(){ + var g = $p("x E @0.264,-0.027\n"+ +"y O @0.537,-0.015\n"+ +"y2 A @0.216,-0.015\n"+ +"\n"+ +"x y2 y"); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "{y2}" ) + +assert.equal((function(){ + var g = TestGraphs.findExample( "Many variables" ); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "{7}\n{8}" ) + +assert.equal((function(){ + var g = TestGraphs.findExample( "Extended confounding" ); + g.addLatentNode( "A" ); + return imp_2_str( GraphAnalyzer.listMinimalImplications( g ) ); +})(), "" ) + +assert.equal((function(){ + var g = TestGraphs.findExample( "Sebastiani" ); + return imp_2_str( GraphAnalyzer.listMinimalImplications( g, 7 ) ); +})(), "EDN1.3 _||_ SELP.22 | SELP.17, Stroke\n"+ + "EDN1.3 _||_ SELP.22 | ECE1.13, Stroke\n"+ + "EDN1.3 _||_ SELP.22 | ECE1.12, Stroke\n"+ + "EDN1.3 _||_ SELP.22 | ANXA2.8\n"+ + "EDN1.3 _||_ SELP.17 | ECE1.13\n"+ + "EDN1.3 _||_ SELP.17 | ECE1.12, Stroke\n"+ + "EDN1.3 _||_ SELP.17 | ANXA2.8" ) + +assert.equal((function(){ + // X -> I -> Y, I <- M -> Y, I -> A = bias + var g = $p("X E\nY O\nI 1\nJ A\nM 1\n\nX I\nI J Y\nM I Y") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "I J Y\nM I Y\nX I" ) + +assert.equal((function(){ + // X -> I -> Y, I <- M -> Y = bias + var g = $p("X E\nY O\nI A\nM 1\n\nX I\nI Y\nM I Y") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "M I Y\nX I" ) + +assert.equal((function(){ + // X -> I -> Y, I -> A = bias + var g = $p("X E\nY O\nI 1\nJ A\n\nX I\nI Y J") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "I J Y\nX I" ) + +assert.equal((function(){ + // A <- X -> Y = no bias + var g = $p("X E\nY O\nI A\n\nX Y\nX I") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + // X -> Y -> A = bias + var g = $p("X E\nY O\nI A\n\nX Y\nY I") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "X Y\nY I" ) + +assert.equal((function(){ + // X -> A -> Y = no bias + var g = $p("X E\nY O\nI A\n\nX I\nI Y") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + return GraphTransformer.activeBiasGraph(TestGraphs.felixadjust).toAdjacencyList() +})(), "s1 s2 z\ns2 s3\ns3 y\nx s1" ) + +assert.equal((function(){ + var g = $p("A E @1,1\nB O @3,1\nC 1 @2,1\n\nA B\nB C") + return g.hasCompleteLayout() +})(), true ) + +assert.equal((function(){ + var g = $p("A E @1,1\nB O\nC 1 @1,1\n\nA B\nB C") + return g.hasCompleteLayout() +})(), false ) + +assert.equal((function(){ + var g = $p("A E\nB O\nC 1\n\nA B\nB C"); + return g.hasCompleteLayout() +})(), false ) + +assert.equal((function(){ + var g = $p("A E\nB O\nC 1\n\nA B\nB C"); + g.deleteVertex(g.getVertex("A")); + return g.getSources().length; +})(), 0 ) + +assert.equal((function(){ + var g = $p("D O\nD2 O\nE E\nE2 E\n\nD E\nD2 E2") + //console.log(g.toString()) + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "D E\nD2 E2" ) + +assert.equal((function(){ + var g = $p("ein A 1\n"+ +"ein B 1\n"+ +"\n"+ +"ein A ein B"); + return g.oldToString(); +})(), "ein%20A 1\nein%20B 1\n\nein%20A ein%20B" ) + +assert.equal((function(){ + var g = $p("A E\nB O\nE E\nZ\nU\n\nA U\nB Z\nZ E\nU Z"); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "{U, Z}" ) + +assert.equal((function(){ + var g = $p("E E\nD O\nA 1\nB U\nZ 1\n\nA E Z\nB D Z\nZ E D\nE D"); + return GraphAnalyzer.directEffectEqualsTotalEffect( g ) +})(), true ) + +assert.equal((function(){ + var g = $p("E E\nD O\nA 1\nB U\nZ 1\n\nA E Z\nB D Z\nZ D\nE D Z"); + return GraphAnalyzer.directEffectEqualsTotalEffect( g ) +})(), false ) + +assert.equal((function(){ + var g = $p("E E\nD O\nA 1\nB U\nZ 1\n\nA E Z\nB D Z\nZ E D"); + return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); +})(), "{A, Z}" ) + +assert.equal((function(){ + var g = $p("E E\nD O\nA 1\nB U\nZ 1\n\nA E Z\nB D Z\nZ E D"); + return sep_2_str( GraphAnalyzer.listMinimalSeparators(GraphTransformer.moralGraph(g)) ); +})(), "{A, Z}\n{B, Z}" ) + +assert.equal((function(){; + var g = TestGraphs.small3(); + g.addAdjustedNode("p"); + var abg = GraphTransformer.activeBiasGraph(g); + return GraphTransformer.edgeInducedSubgraph(g,abg.edges).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + var g = $p("E E @-1.897,0.342\n"+ +"D O @-0.067,0.302\n"+ +"g A @-0.889,1.191\n"+ +"\n"+ +"D g\n"+ +"E D"); + return _.pluck(GraphAnalyzer.nodesThatViolateAdjustmentCriterion( g ),'id').join(","); +})(), "g" ) + +assert.equal((function(){ + var g = $p("X1 E\nX2 E\nY1 O\nY2 O\nU1 1\nU2 1\n\nU1 X1 X2\nU2 Y1 Y2") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + var g = $p("A E\nB O\nC O\nD A\n\nA B\nB C\nC D") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "A B\nB C\nC D" ) + +assert.equal((function(){ + var g = $p("A E\nB O\nC O\n\nA B\nB C") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY1 Y2\nY2 D") + return GraphAnalyzer.directEffectEqualsTotalEffect( g ) +})(), true ) + +assert.equal((function(){ + var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY2 D") + return GraphAnalyzer.directEffectEqualsTotalEffect( g ) +})(), true ) + +assert.equal((function(){ + var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY1 Y2\nY2 D") + return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) +})(), "" ) + +assert.equal((function(){ + var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY2 D") + return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) +})(), "{D}" ) + +assert.equal((function(){ + var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY1 Y2\nY2 D") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "X1 Y1\nY1 Y2\nY2 D" ) + +assert.equal((function(){ + var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY2 D") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + var g = $p("X1 E\nX2 E\nY1 O\nY2 O\n\nX1 Y1 X2\nY2 X2") + //console.log(g.toString()) + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "Y2 X2" ) + +assert.equal((function(){ + var g = $p("X1 E\nX2 E\n\nX1 X2") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + var g = $p("X1 E\nX2 E\nY1 O\nY2 O\n\nX1 Y1 X2\nX2 Y2") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + var g = $p("X1 E\nX2 E\nY1 O\nY2 O\n\nX1 Y1 X2\nX2 Y2") + return _.pluck( + _.difference(GraphAnalyzer.properPossibleCausalPaths(g),g.getSources()), + "id").sort().join(",") +})(), "Y1,Y2" ) + + +assert.equal((function(){ + // X -> I -> Y, I -> A, adjust I = no bias + var g = $p("X E\nY O\nI A\nJ A\n\nX I\nI J Y") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + // X -> I -> Y, I <- M -> Y, I -> A, adjust M, I = no bias + var g = $p("X E\nY O\nI A\nJ A\nM A\n\nX I\nI J Y\nM I Y") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + // X -> I -> Y, I <- M -> Y, I -> A, adjust M = bias + var g = $p("X E\nY O\nI 1\nJ A\nM A\n\nX I\nI J Y\nM I Y") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "I J Y\nX I" ) + + +assert.equal((function(){ + var g = TestGraphs.findExample( "Shrier" ) + var r = "" + var v1,v2,vv=g.getVertices() + for( var i = 0 ; i < vv.length ; i ++ ){ + for( var j = 0 ; j < vv.length ; j ++ ){ + var c = GraphAnalyzer.minVertexCut( g, [vv[i]], [vv[j]] ) + if( c > 0 ) r += vv[i].id + " " + vv[j].id + " " + c + "\n" + } + } + return r +})(), "WarmUpExercises Injury 1\nCoach WarmUpExercises 2\nCoach Injury 2\nCoach PreGameProprioception 1\nCoach PreviousInjury 1\nCoach IntraGameProprioception 2\nCoach NeuromuscularFatigue 1\nGenetics WarmUpExercises 1\nGenetics Injury 3\nGenetics PreGameProprioception 1\nGenetics TissueWeakness 1\nGenetics IntraGameProprioception 2\nTeamMotivation Injury 1\nTeamMotivation IntraGameProprioception 1\nPreGameProprioception Injury 1\nPreGameProprioception IntraGameProprioception 1\nConnectiveTissueDisorder Injury 2\nConnectiveTissueDisorder IntraGameProprioception 1\nContactSport Injury 1\nFitnessLevel WarmUpExercises 1\nFitnessLevel Injury 2\nFitnessLevel IntraGameProprioception 2\n" ) + +assert.equal((function(){ + var g = $p("X E\nY O\nM 1\nN A\n\nX M\nM N Y") + return GraphTransformer.flowNetwork(g).graph.toAdjacencyList() +})(), "M N X Y\nN M\nX M __SRC\nY M __SNK\n__SNK Y\n__SRC X" ) + +assert.equal((function(){ + var g = $p("X E\nY O\nM 1\nN A\n\nX M\nM N Y") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "M N Y\nX M" ) + +assert.equal((function(){ + var g = $p("X E\nY O\nM 1\nN A\n\nX M\nM N\nY M") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "X M\nY M" ) + +assert.equal((function(){ + // our non-X-ancestor MSAS example from the UAI paper w/o causal edge + var g = $p("X1 E\nX2 E\nY O\nM1 1\nM2 A\n\nX1 M1\nY M2\nM1 M2\nM2 X2") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "M1 M2\nX1 M1\nY M2" ) + +assert.equal((function(){ + var g = $p("X E\nY O\nM 1\n\nY M\nM X") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "M X\nY M" ) + +assert.equal((function(){ + var g = $p("X E\nY O\n\nY X") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "Y X" ) + +assert.equal((function(){ + // our non-X-ancestor MSAS example from the UAI paper w/o causal edge + var g = $p("X1 E\nX2 E\nY O\nM1 1\nM2 1\n\nX1 M1\nY M2\nM1 M2\nM2 X2") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "M2 X2\nY M2" ) + +assert.equal((function(){ + var g = $p("X1 E\nY1 O\nY2 O\nM 1\n\nY1 M\nM Y2") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + +assert.equal((function(){ + var g = $p("X1 E\nX2 E\nY1 O\nY2 O\nU1 1\nU2 1\n\nX1 U1\nU1 X2\nY1 U2\nU2 Y2") + return GraphTransformer.activeBiasGraph(g).toAdjacencyList() +})(), "" ) + + +assert.equal((function(){ + var g = TestGraphs.findExample( "Thoemmes" ) + return GraphAnalyzer.minVertexCut( g, + [g.getVertex("e0"),g.getVertex("s2")], + [g.getVertex("y")] ) +})(), 2 ) + +assert.equal((function(){ + var g = $p("a 1\nb 1\nc 1\nd 1\ne 1\nm 1\nn 1\np 1\nu 1\nx E\ny O\n\n" + +"a b\nb u\nc u n\nd e\ne c\nm a\nn p\np y\nu y\nx c d m") + return GraphAnalyzer.minVertexCut( g, [g.getVertex("x")], [g.getVertex("y")] ) +})(), 2 ) + +assert.equal((function(){ + var g = TestGraphs.findExample( "Confounding" ) + var r = "" + var v1,v2,vv=g.getVertices() + for( var i = 0 ; i < vv.length ; i ++ ){ + for( var j = 0 ; j < vv.length ; j ++ ){ + var c = GraphAnalyzer.minVertexCut( g, [vv[i]], [vv[j]] ) + if( c > 0 ) r += vv[i].id + " " + vv[j].id + " " + c + "\n" + } + } + return r +})(), "A D 2\nB E 1\n" ) + +assert.equal((function(){ + var g = $p( + "digraph G { y -> m \n m -> x }" + ).addSource("x").addTarget("y") + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "m -> x\ny -> m" ) + +assert.equal((function(){ + var g = $p( + "digraph G { x [exposure]\ny [outcome]\na [adjusted]\n"+ + "x -> a\nu -> x\nu -> y }" ) + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "u -> x\nu -> y" ) + +assert.equal((function(){ + var g = $p( + "digraph G { x [exposure]\ny [outcome]\na [adjusted]\n b [adjusted]\n"+ + "x -> a\nx -> b\nb -> a\ny -> b }" ) + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "x -> b\ny -> b" ) + +assert.equal((function(){ + var g = $p( + "digraph G { x [exposure]\ny [outcome]\nm [adjusted]\nx -> m\ny -> m }" ) + g = GraphTransformer.canonicalDag( g ).g + g = GraphTransformer.ancestorGraph( g ) + return ""+$es(g) +})(), "x -> m\ny -> m" ) + +assert.equal((function(){ + var g = $p( + "digraph G { x [exposure]\ny [outcome]\nm [adjusted]\nx -> m\ny -> m }" ) + return $es(GraphTransformer.ancestorGraph(g)) +})(), "x -> m\ny -> m" ) + +assert.equal((function(){ + var g = $p( + "digraph G { b [exposure]\nc [outcome]\na [adjusted]\na -- b\na -- c }" ) + return GraphAnalyzer.connectedComponents(g).length +})(), 1 ) + +assert.equal((function(){ + var g = $p( + "digraph G {a -- b }" ) + return GraphAnalyzer.connectedComponents(g).length +})(), 1 ) + +assert.equal((function(){ + var g = $p( + "digraph G {a [exposure]\nb [outcome]\nc [adjusted]\na -> c\nb -> c }" ) + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "a -> c\nb -> c" ) + +assert.equal((function(){ + var g = $p( + "digraph G {a [exposure]\nb [outcome]\nc [adjusted]\na -> b\nb -> c }" ) + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "a -> b\nb -> c" ) + +assert.equal((function(){ + var g = TestGraphs.findExample("Acid") + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "x1 -> x3\nx1 -> x4\nx10 -> x15\nx4 -> x5\nx5 -> x7\nx7 -> x9\nx9 -> x10" ) + +assert.equal((function(){ + var g = $p( + "digraph G { a [exposure] \n b [outcome] \n a <-> c \n c -> b }" ) + return $es(GraphTransformer.canonicalDag(g).g) +})(), "L1 -> a\nL1 -> c\nc -> b" ) + +assert.equal((function(){ + var g = $p( + "digraph G { a [exposure] \n b [outcome] \n a <-> c \n c -> b }" ) + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "a <-> c\nc -> b" ) + +assert.equal((function(){ + var g = $p( "digraph G { a -- b }" ) + var ids = _.pluck(GraphAnalyzer.connectedComponentAvoiding( g, [g.getVertex("a")] ) + ,"id") + ids=ids.sort(); return ids.join(" ") +})(), "a b" ) + +assert.equal((function(){ + var g = $p( + "digraph G { x [source] \n y [outcome] \n a [adjusted] \n " + +"a -> x \n b -> a \n b -> y }" + ) + return $es(GraphTransformer.activeBiasGraph(g)) +})(), "" ) + + +assert.equal( _.pluck(GraphAnalyzer.dpcp( + $p( "digraph{ x [exposure]\n y [outcome]\n x -> y -- z }" ) ),"id"). + sort().join(","), + "y,z" ) + +assert.equal( _.pluck(GraphAnalyzer.dpcp( + $p( "digraph{ x [exposure]\n y [outcome]\n x -- y -- z }" ) ),"id"). + sort().join(","), + "x,y,z" ) + + +}); // end uncategorized tests + diff --git a/test/test/pags.js b/test/test/pags.js new file mode 100644 index 0000000..d467ed0 --- /dev/null +++ b/test/test/pags.js @@ -0,0 +1,62 @@ + +const {GraphParser, + GraphAnalyzer, + GraphTransformer} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") + +const $p = (s) => GraphParser.parseGuess(s) + + +QUnit.module('dagitty') + +QUnit.test('miscellaneous PAG tests', assert => { + let g = GraphParser.parseGuess( "a -> b <- c" ) + assert.false( GraphAnalyzer.dConnected( g, ["a"], ["c"] ) ) + assert.true( GraphAnalyzer.dConnected( g, ["a"], ["c"], ["b"] ) ) + + g = GraphParser.parseGuess( "pag { a @-> b <-@ c }" ) + assert.false( GraphAnalyzer.dConnected( g, ["a"], ["c"] ) ) + assert.true( GraphAnalyzer.dConnected( g, ["a"], ["c"], ["b"] ) ) + + assert.false( GraphAnalyzer.dConnected( g, [], ["a"] ) ) + + + assert.equal( GraphAnalyzer.listMsasTotalEffect( + TestGraphs.spirtes, [], [], 200 ).length, 200 ) + + assert.equal( GraphAnalyzer.violatesAdjustmentCriterion( + TestGraphs.spirtes ), false ) + + let gam = GraphTransformer.moralGraph( + GraphTransformer.ancestorGraph( + GraphTransformer.backDoorGraph( TestGraphs.spirtes ) ) ) + + assert.equal( gam.edges.length, 302 ) + + assert.equal( GraphTransformer.ancestorGraph( + GraphTransformer.backDoorGraph( + TestGraphs.spirtes ) ).edges.length, 133 ) + + + assert.equal( GraphTransformer.backDoorGraph( + TestGraphs.spirtes ).edges.length, 795 ) + + assert.equal( + GraphTransformer.backDoorGraph( + $p("pag{ {V2 V1} @-> X -> {V4 @-> Y} <- V3 @-> X X[e] Y[o]}")). + getVertex("X").getChildren().length, 0, "No children of X" ) + + assert.equal( + GraphTransformer.backDoorGraph( + $p("pag{ {V2 V1} -> X -> {V4 -> Y} <- V3 -> X X[e] Y[o]}")). + getVertex("X").getChildren().length, 0 ) + + assert.equal( + GraphTransformer.backDoorGraph( + $p("pdag{ {V2 V1} -> X -> {V4 -> Y} <- V3 -> X X[e] Y[o]}")). + getVertex("X").getChildren().length, 0 ) + + assert.equal( + GraphAnalyzer.dpcp($p("pag{ {V2 V1} -> X -> V4 -> Y <- V3 <-> {X V4} X[e] Y[o]}")).length, 2 ) + +} ) diff --git a/test/test/parser.js b/test/test/parser.js new file mode 100644 index 0000000..148e751 --- /dev/null +++ b/test/test/parser.js @@ -0,0 +1,233 @@ +const { + Graph, + GraphSerializer, + GraphParser, + GraphTransformer +} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") + +const _ = require("underscore") + +GraphParser.VALIDATE_GRAPH_STRUCTURE = true +const $p = function(s){ return GraphParser.parseGuess(s) } +const $es = function(g){ return GraphSerializer.toDotEdgeStatements(g) } + +QUnit.module('dagitty') + +QUnit.test( "parsing and serializing", function( assert ) { + // GraphParser.VALIDATE_GRAPH_STRUCTURE = false; + // + // + + assert.equal( (new Graph("dag{a->b}")).vertices.values().length, 2, "DAG construction from dot string" ) + assert.equal( (new Graph(" dag { a -> b } ")).vertices.values().length, 2, "DAG construction from dot string" ) + assert.equal( (new Graph("a->b")).vertices.values().length, 2, "DAG construction from arrow string" ) + + + assert.equal( $es( $p( "map -> pop" ) ), "map -> pop", "reserved words" ) + + assert.equal( $es( $p( "x -> y" ) ), "x -> y", "shorthand notation" ) + + assert.equal( $p( "dag{{x->{a b}}}" ).edges.length, 2, "grouping" ) + + assert.equal( $p( "dag{{x->{a ; b}}}" ).edges.length, 2, + "semicolons" ) + assert.equal( $p( "dag{a;b}" ).getVertices().length, 2, + "semicolons 2" ) + assert.equal( $p( "dag{;b}" ).getVertices().length, 1, + "semicolons 2" ) + assert.equal( $p( "dag{a;}" ).getVertices().length, 1, + "semicolons 2" ) + + assert.equal( $p( "dag{}" ).getVertices().length, 0, + "semicolons 2" ) + assert.equal( $p( "dag{;}" ).getVertices().length, 0, + "semicolons 2" ) + assert.equal( $p( "dag{;;;}" ).getVertices().length, 0, + "semicolons 2" ) + + assert.equal( $p( "dag{a}" ).getVertices().length, + 1, "spaces" ) + assert.equal( $p( "dag{a }" ).getVertices().length, + 1, "spaces" ) + assert.equal( $p( "dag{ a }" ).getVertices().length, + 1, "spaces" ) + + + assert.equal( $p( "pag{a @-> {b --@ {c @-@ d} }}" ).edges.length, 6 ) + assert.equal( _.pluck($p( "pag{a @->b<-@c}" ).getVertices(),"id").sort().join(","), "a,b,c" ) + assert.equal( _.pluck($p( "pag{a @->b<-@c @-@d}" ).getVertices(),"id").sort().join(","), "a,b,c,d" ) + + assert.equal( $p( "dag{x->{a b}}" ).edges.length, 2 ) + + assert.equal( $p( "dag{x->{a->b}}" ).edges.length, 3 ) + + assert.equal( $p( "dag{a->{b->{c->{d->e}}}}" ).edges.length, 10 ) + + assert.equal( GraphSerializer.toDotVertexStatements( $p( + "digraph G { \"ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ\" }" ) ).trim(), + "\"ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ\"" ) + + assert.equal( GraphSerializer.toDotVertexStatements( $p( + "digraph G { x [label=\"από το Άξιον Εστί\"] }" ) ).trim(), + "x [label=\"από το Άξιον Εστί\"]" ) + + assert.equal( GraphSerializer.toDot( $p( + "digraph G { graph [bb=\"0,0,100,100\"] }" ) ).trim(), + "digraph G{\nbb=\"0,0,100,100\"\n\n}" ) + + assert.equal( TestGraphs.small1().oldToString(), "A 1\nS E\nT O\n\nA S\nS T" ) + + assert.equal((function(){ + var g = $p( + "digraph G { x <-> y }" + ).addSource("x").addTarget("y") + GraphTransformer.activeBiasGraph( g ) + return g.edges[0].directed + })(), Graph.Edgetype.Bidirected ) + + assert.equal((function(){ + var g = $p( + "digraph G { xi1 [latent]\n"+ + "xi1 -> x1\n"+ + "xi1 -> x3\n"+ + "xi1 -> x4\n"+ + "xi1 -> x5\n"+ + "x5 -> x6\n"+ + "x1 -> x6\n"+ + "x6 -> x3 }\n" + ) + return g.oldToString() + })(), "x1 1\nx3 1\nx4 1\nx5 1\nx6 1\nxi1 U\n\nx1 x6\nx5 x6\nx6 x3\nxi1 x1 x3 x4 x5" ) + + assert.equal((function(){ + var g = GraphParser.parseDot( + "digraph { xi1 [latent]\n"+ + "xi2 [latent]\n"+ + "xi1 <-> xi2\n"+ + "xi1 -> x1\n"+ + "xi1 -> x2\n"+ + "xi1 -> x3\n"+ + "xi2 -> x4\n"+ + "xi2 -> x5\n"+ + "xi2 -> x6 } " + ) + return g.oldToString() + })(), "x1 1\nx2 1\nx3 1\nx4 1\nx5 1\nx6 1\nxi1 U\nxi2 U\n\nxi1 x1 x2 x3 xi2\nxi2 x4 x5 x6 xi1", "old syntax still works" ) + + assert.equal((function(){ + var g = GraphParser.parseDot( "digraph { x -> y\ny -> z\nx <-> y } " ) + return $es(g) + })(), "x -> y\nx <-> y\ny -> z" ) + + + assert.equal((function(){ + var g = $p( + "digraph G {\ny -- x -> y <-> x }" ) + return $es(g) + })(), "x -- y\nx -> y\nx <-> y" ) + + + assert.equal((function(){ + var g = $p( + "graph G {\nx [ exposure , pos =\" 12. , .13 \"]\ny [outcome]\n}" ) + return GraphSerializer.toDot(g) + })(), "graph G{\nx [exposure,pos=\"12.000,0.130\"]\ny [outcome]\n\n}\n" ) + + + assert.equal((function(){ + var g = $p( + "graph G { x [exposure] \n y [outcome] }" ) + g.getVertex("x").layout_pos_x = 1.0 + g.getVertex("x").layout_pos_y = 1.0 + return GraphSerializer.toDot(g) + })(), "graph G{\nx [exposure,pos=\"1.000,1.000\"]\ny [outcome]\n\n}\n" ) + + assert.equal((function(){ + var g = $p( + "graph G { x \n y }" ) + return GraphSerializer.toDot(g) + })(), "graph G{\nx\ny\n\n}\n" ) + + assert.equal((function(){ + var g = $p( + "graph G { x [] \n y [] }" ) + return GraphSerializer.toDot(g) + })(), "graph G{\nx\ny\n\n}\n" ) + + assert.equal((function(){ + var g = $p( + "digraph G { x [] \n y [] }" ) + g.addEdge("x","y",Graph.Edgetype.Directed) + g.addEdge("x","y",Graph.Edgetype.Undirected) + g.addEdge("x","y",Graph.Edgetype.Bidirected) + return $es(g) + })(), "x -- y\nx -> y\nx <-> y" ) + + assert.equal((function(){ + var g = $p( "digraph G { x <-> m } " ) + return typeof g.getEdge("m","x",2) + })(), "object" ) + + assert.equal((function(){ + var g = $p( "digraph G { x <-> m } " ) + return typeof g.getEdge("x","m",2) + })(), "object" ) + + assert.equal((function(){ + var g = $p( "digraph G { x -> m } " ) + return $es(g) + })(), "x -> m" ) + + assert.equal((function(){ + var g = $p( "dag G { M [pos=\"-0.521,-0.265\"] \n "+ + "X [exposure,pos=\"-1.749,-0.238\"] \n "+ + "Y [outcome,pos=\"1.029,-0.228\"] \n "+ + "M <-> Y [pos=\"0.645,-0.279\"] \n "+ + "X -> Y \n "+ + "X -> M -> Y } " ) + return GraphSerializer.toDot(g) + })(), "dag G{\nM [pos=\"-0.521,-0.265\"]\n"+ + "X [exposure,pos=\"-1.749,-0.238\"]\n"+ + "Y [outcome,pos=\"1.029,-0.228\"]\n"+ + "M -> Y\n"+ + "M <-> Y [pos=\"0.645,-0.279\"]\n"+ + "X -> M\n"+ + "X -> Y\n}\n" ) + + assert.equal((function(){ + var g = $p( "dag G { M [pos=\"-0.521,-0.265\"] "+ + "X [exposure,pos=\"-1.749,-0.238\"] "+ + "Y [outcome,pos=\"1.029,-0.228\"] "+ + "M <-> Y [pos=\"0.645,-0.279\"] "+ + "X -> Y "+ + "X -> M -> Y } " ) + return GraphSerializer.toDot(g) + })(), "dag G{\nM [pos=\"-0.521,-0.265\"]\n"+ + "X [exposure,pos=\"-1.749,-0.238\"]\n"+ + "Y [outcome,pos=\"1.029,-0.228\"]\n"+ + "M -> Y\n"+ + "M <-> Y [pos=\"0.645,-0.279\"]\n"+ + "X -> M\n"+ + "X -> Y\n}\n" ) + + assert.equal((function(){ + var g = $p( + "digraph G { x -- y \n x -> y \n x <-> y [pos=\"1.000,1.000\"] }" ) + return $es(g) + })(), "x -- y\nx -> y\nx <-> y [pos=\"1.000,1.000\"]" ) + + assert.equal((function(){ + var g = $p( + "digraph G {\n y -- x -> y <-> x [pos=\"1.0,0.1\"] }" ) + return $es(g) + })(), "x -- y [pos=\"1.000,0.100\"]\nx -> y [pos=\"1.000,0.100\"]\nx <-> y [pos=\"1.000,0.100\"]" ) + + assert.equal(GraphSerializer.toLavaan($p("dag{x1}")).split("\n")[1] + ,"x1 ~~ x1") + + assert.equal( $es($p( + "dag{ U -> {a b c d} }" )), "U -> a\nU -> b\nU -> c\nU -> d" ) + +} ) + diff --git a/test/test/polynomials.js b/test/test/polynomials.js new file mode 100644 index 0000000..e057bf7 --- /dev/null +++ b/test/test/polynomials.js @@ -0,0 +1,115 @@ +const { MPoly } = require("../../jslib/dagitty-node.js") + +QUnit.test( "multivariate polynomials", function( assert ) { + assert.equal(MPoly("0"), "0") + assert.equal(MPoly("1"), "1") + assert.equal(MPoly("2"), "2") + assert.equal(MPoly("-1"), "-1") + assert.equal(MPoly("-2"), "-2") + assert.equal(MPoly("1.234E8"), "123400000") + assert.equal(MPoly("-1.234e8"), "-123400000") + assert.equal(MPoly("a"), "a") + assert.equal(MPoly("b"), "b") + assert.equal(MPoly("abc123"), "abc123") + assert.equal(MPoly("x"), "x") + assert.equal(MPoly("x^2"), "x^2") + assert.equal(MPoly("x^2 y^3"), "x^2 y^3") + assert.equal(MPoly("y^3 x^2"), "x^2 y^3") + assert.equal(MPoly("x x"), "x^2") + assert.equal(MPoly("x x * x * x"), "x^4") + assert.equal(MPoly("y x^2"), "x^2 y") + assert.equal(MPoly("y x^2 z"), "x^2 y z") + assert.equal(MPoly("3x"), "3 x") + assert.equal(MPoly(" 3 x"), "3 x") + assert.equal(MPoly(" 3e2 x"), "300 x") + assert.equal(MPoly(" 3e2 * x"), "300 x") + assert.equal(MPoly(" 3e2 x y"), "300 x y") + assert.equal(MPoly(" 3e2 x^5 y^6"), "300 x^5 y^6") + assert.equal(MPoly("-x^2"), "-x^2") + assert.equal(MPoly("- x^2"), "-x^2") + assert.equal(MPoly("x + y"), "x + y") + assert.equal(MPoly("x + 2y"), "x + 2 y") + assert.equal(MPoly("x+2y"), "x + 2 y") + assert.equal(MPoly("a^2 b^2 + 0x"), "a^2 b^2") + assert.equal(MPoly("a^2 b^2 + 123x"), "a^2 b^2 + 123 x") + assert.equal(MPoly("-2y"), "-2 y") + assert.equal(MPoly("x-2y"), "x - 2 y") + assert.equal(MPoly("x^4-2y^5"), "x^4 - 2 y^5") + assert.equal(MPoly("x^4-2y^5-z^7"), "x^4 - 2 y^5 - z^7") + assert.equal(MPoly("xy + abc123"), "abc123 + xy") + assert.equal(MPoly("xy*abc*def"), "abc def xy") + + assert.equal(MPoly("x").negate(), "-x") + assert.equal(MPoly("x+y").negate(), "-x - y") + assert.equal(MPoly("x + 4 y").negate(), "-x - 4 y") + + assert.equal(MPoly("x").add(MPoly("y")), "x + y") + assert.equal(MPoly("x + y").add(MPoly("y + z")), "x + 2 y + z") + assert.equal(MPoly("x + y").add(MPoly("-y + z")), "x + z") + assert.equal(MPoly("x - y - z").add(MPoly("y - z")), "x - 2 z") + assert.equal(MPoly("z").add(MPoly("a + x")), "a + x + z") + + assert.equal(MPoly.add(), "0") + assert.equal(MPoly.add(MPoly("x")), "x") + assert.equal(MPoly.add(MPoly("x"), MPoly("y")), "x + y") + assert.equal(MPoly.add(MPoly("x"), MPoly("y"), MPoly("x")), "2 x + y") + + assert.equal(MPoly("100 y^3").mul(MPoly("217")), "21700 y^3") + assert.equal(MPoly("100 y^3").mul(MPoly("2 x^100 z^7 zz^8")), "200 x^100 y^3 z^7 zz^8") + assert.equal(MPoly("100 y^3 zzzz^9").mul(MPoly("2 x^100 y z^7 zz^8")), "200 x^100 y^4 z^7 zz^8 zzzz^9") + assert.equal(MPoly("x + y").mul(MPoly("z")), "x z + y z") + assert.equal(MPoly("x + y").mul(MPoly("x y")), "x y^2 + x^2 y") + assert.equal(MPoly("x + y").mul(MPoly("x + y")), "2 x y + x^2 + y^2") + assert.equal(MPoly("x + y").mul(MPoly("0 x")), "0") + assert.equal(MPoly("- x").mul(MPoly("- x")), "x^2") + + assert.equal(MPoly("0").isZero(), true) + assert.equal(MPoly("1").isZero(), false) + assert.equal(MPoly("x + y + z").isZero(), false) + assert.equal(MPoly("0 x + 0 y + 0*z").isZero(), true) + + assert.equal(MPoly("x+y").sub(MPoly("x")), "y") + assert.equal(MPoly("x+y").sub(MPoly("y")), "x") + assert.equal(MPoly("x+y").sub(MPoly("x+y")), "0") + assert.equal(MPoly("x").sub(MPoly("x")).isZero(), true) + assert.equal(MPoly("x").sub(MPoly("x + y")).isZero(), false) + + + assert.equal(MPoly("0").eval({"x": MPoly("7")}), "0") + assert.equal(MPoly("x").eval({"x": MPoly("7")}), "7") + assert.equal(MPoly("x").eval({"x": MPoly("y")}), "y") + assert.equal(MPoly("x").eval({"x": MPoly("x + y")}), "x + y") + assert.equal(MPoly("x^2").eval({"x": MPoly("x + y")}), "2 x y + x^2 + y^2") + assert.equal(MPoly("x y").eval({"x": MPoly("y")}), "y^2") + assert.equal(MPoly("x y").eval({"x": MPoly("x + y")}), "x y + y^2") + assert.equal(MPoly("x^2 y").eval({"x": MPoly("x + y")}), "2 x y^2 + x^2 y + y^3") + assert.equal(MPoly("x y + z + a").eval({"x": MPoly("y")}), "a + y^2 + z") + assert.equal(MPoly("x y + z + a").eval({"x": MPoly("x + y")}), "a + x y + y^2 + z") + assert.equal(MPoly("x^2 y + z + a").eval({"x": MPoly("x + y")}), "a + 2 x y^2 + x^2 y + y^3 + z") + assert.equal(MPoly("x^2 y + z + a").eval({"x": MPoly("2 x + 10 y")}), "a + 40 x y^2 + 4 x^2 y + 100 y^3 + z") + assert.equal(MPoly("x^2 y + z + a").eval({"x": MPoly("x + y"), "z": MPoly("3 a^2"), "a": MPoly("b^7")}), "3 a^2 + b^7 + 2 x y^2 + x^2 y + y^3") + assert.equal(MPoly("x^3").eval({"x": MPoly("x + y")}), "3 x y^2 + 3 x^2 y + x^3 + y^3") + assert.equal(MPoly("x^3").eval({"x": MPoly("2 x + 10 y")}), "600 x y^2 + 120 x^2 y + 8 x^3 + 1000 y^3") + assert.equal(MPoly("x^4").eval({"x": MPoly("x + y")}), "4 x y^3 + 6 x^2 y^2 + 4 x^3 y + x^4 + y^4") + assert.equal(MPoly("x^4").eval({"x": MPoly("2 x + 10 y")}), "8000 x y^3 + 2400 x^2 y^2 + 320 x^3 y + 16 x^4 + 10000 y^4") + + + assert.equal(MPoly("0").evalToNumber({"x": 7}), 0) + assert.equal(MPoly("123").evalToNumber({"x": 7}), 123) + assert.equal(MPoly("x").evalToNumber({"x": 7}), 7) + assert.equal(MPoly("x^2").evalToNumber({"x": 7}), 49) + assert.equal(MPoly("x y").evalToNumber({"x": 7, "y": 3}), 21) + assert.equal(MPoly("x y + z + a").evalToNumber({"x": 7, "y": 2, "z": 1000, "a": 200}), 1214) + assert.equal(MPoly("x^3").evalToNumber({"x": 3}), 27) + assert.equal(MPoly("x^4").evalToNumber({"x": 3}), 81) + + assert.equal(MPoly("0").evalToBigInt({"x": 7n}), 0) + assert.equal(MPoly("123").evalToBigInt({"x": 7n}), 123) + assert.equal(MPoly("x").evalToBigInt({"x": 7n}), 7) + assert.equal(MPoly("x^2").evalToBigInt({"x": 7n}), 49) + assert.equal(MPoly("x y").evalToBigInt({"x": 7n, "y": 3n}), 21) + assert.equal(MPoly("x y + z + a").evalToBigInt({"x": 7n, "y": 2n, "z": 1000n, "a": 200n}), 1214) + assert.equal(MPoly("x^3").evalToBigInt({"x": 3n}), 27) + assert.equal(MPoly("x^4").evalToBigInt({"x": 3n}), 81) +}) + diff --git a/test/test/selection-nodes.js b/test/test/selection-nodes.js new file mode 100644 index 0000000..2278cdc --- /dev/null +++ b/test/test/selection-nodes.js @@ -0,0 +1,18 @@ + +const dagitty = require("../../jslib/dagitty-node.js") + +QUnit.module('dagitty') + +QUnit.test('adjustment in DAG with selection nodes', assert => { + let g = dagitty.GraphParser.parseGuess( "S1 -> {X[exposure]} -> {Y[outcome]} <- S2" ) + + assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, [] ), true ) + assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, [], [] ), true ) + assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, [], ["S1"] ), true ) + + + assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, [], ["S2"] ), false ) + assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, ["S2"], ["S1"] ), true ) + assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, ["S1"], ["S2"] ), false ) + +}); diff --git a/test/test/separators.js b/test/test/separators.js new file mode 100644 index 0000000..f373afe --- /dev/null +++ b/test/test/separators.js @@ -0,0 +1,97 @@ + +const {Graph,GraphAnalyzer,GraphParser,GraphTransformer} = require("../../jslib/dagitty-node.js") +const _ = require("underscore") +const TestGraphs = require("../test-graphs.js") + +const $p = (s) => GraphParser.parseGuess(s) +const sep_2_str = (ss) => { + var r = []; + if( ss.length == 0 ) + return ""; + _.each( ss, function(s){ + var rs = _.pluck( s, 'id').sort().join(", "); + r.push(rs); + }); + r.sort(); + return "{"+r.join("}\n{")+"}"; +} + +QUnit.module('dagitty') + +QUnit.test( "separators", function( assert ) { + let g, gm + function verts(a) { + return _.isArray(a) ? a.map(function(vid){return g.getVertex(vid)}) : [g.getVertex(a)] + } + + g = $p("length -> push -> pop map -> { length push }") + assert.equal( GraphAnalyzer.listMinimalImplications( g ).length, 2, "reserverd words" ) + + g = GraphTransformer.backDoorGraph(TestGraphs.small1()) + assert.equal( sep_2_str( GraphAnalyzer.listMinimalSeparators(g) ), "{}" ) + assert.strictEqual( sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g) ] ) , "{}" ) + + g = GraphTransformer.backDoorGraph(TestGraphs.confounding_triangle()) + gm = GraphTransformer.moralGraph(GraphTransformer.ancestorGraph(g)) + assert.equal( sep_2_str( GraphAnalyzer.listMinimalSeparators(gm) ), "{C}" ) + assert.equal( sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g) ] ), "{C}" ) + + + g = GraphTransformer.backDoorGraph(TestGraphs.m_bias_graph()) + gm = GraphTransformer.moralGraph(GraphTransformer.ancestorGraph(g)) + assert.equal( sep_2_str ( GraphAnalyzer.listMinimalSeparators(gm) ), "{}" ) + assert.equal( sep_2_str( [ GraphAnalyzer.findMinimalSeparator(gm) ] ) , "{}" ) + + g = GraphTransformer.backDoorGraph(TestGraphs.extended_confounding_triangle()) + gm = GraphTransformer.moralGraph(GraphTransformer.ancestorGraph(g)) + assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm) ), "{C, D}\n{C, E}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g) ] ) , "{C, D}") + + assert.equal(sep_2_str(GraphAnalyzer.listMinimalSeparators( gm, verts(["D"]), [] ) ), "{C, D}" ) + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, verts(["D"]), []) ] ) , "{C, D}") + + assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm, [], [], 1) ), "{C, E}") + assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm, [], verts(["D"])) ), "{C, E}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["D"]) ) ] ) , "{C, E}") + + g = $p("digraph {"+ + "D [outcome] "+ + "E [exposure] "+ + "v1 v2 v3 v4 v5 v6 v7 "+ + "D <-> v5 "+ + "E -> v1 "+ + "E -> v6 "+ + "v1 -> D "+ + "v2 -> D "+ + "v2 -> E "+ + "v3 -> E "+ + "v3 <-> v4 "+ + "v4 -> v6 "+ + "v5 -> v4 "+ + "v6 -> v7 "+ + "v7 -> D"+ + "}") + + gm = GraphTransformer.moralGraph(GraphTransformer.ancestorGraph(g)) + assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm) ), "{v1, v2, v3, v4, v6}\n{v1, v2, v3, v4, v7}\n{v1, v2, v5, v6}\n{v1, v2, v5, v7}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g) ] ), "{v1, v2, v3, v4, v6}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("D"), verts("E")) ] ), "{v1, v2, v5, v7}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, verts("v7")) ] ), "{v1, v2, v3, v4, v7}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, verts("v5")) ] ), "{v1, v2, v5, v6}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, verts(["v5","v7"])) ] ), "{v1, v2, v5, v7}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v7"])) ] ), "{v1, v2, v3, v4, v6}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v6"])) ] ), "{v1, v2, v3, v4, v7}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v4"])) ] ), "{v1, v2, v5, v6}") + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v4","v6"])) ] ), "{v1, v2, v5, v7}") + assert.strictEqual(GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v2"])) , false) + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("v4"), verts("v2")) ]) , "{}" ) + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("v4"), verts("v2"), verts("D"), []) ]) , "{D, E, v3, v5, v6}" ) + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("v4"), verts("v2"), verts(["D","v7"]), []) ]) , "{D, E, v3, v5, v7}" ) + assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("v4"), verts("v2"), verts("D"), verts("v6")) ]) , "{D, E, v3, v5, v7}" ) + + gm.setSources(verts("v4")) + gm.setTargets(verts("v2")) + assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm, verts("D"), []) ) , "{D, E, v3, v5, v6}\n{D, E, v3, v5, v7}" ) +} ); + + diff --git a/test/test/testable-implications.js b/test/test/testable-implications.js new file mode 100644 index 0000000..e6382ad --- /dev/null +++ b/test/test/testable-implications.js @@ -0,0 +1,72 @@ +const {GraphAnalyzer} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") +const _ = require("underscore") + +const imp_2_str = ( imp ) => { + var r = [],j,rr; + _.each( imp, function( i ){ + for( j = 0 ; j < i[2].length ; j ++ ){ + rr = i[0]+" _||_ "+i[1]; + if( i[2][j].length > 0 ){ + rr += " | "+_.pluck( i[2][j], 'id').sort().join(", "); + } + r.push(rr) + } + } ); + return r.join("\n"); +} + +QUnit.module("dagitty") + +QUnit.test( "testable implications", function( assert ) { + assert.equal((function(){ + var g = TestGraphs.findExample("Shrier") + var ii = GraphAnalyzer.listMinimalImplications( g ) + var all_good=true + for( var i = 0 ; i < ii.length ; i ++ ){ + for( var j = 0 ; j < ii[i][2].length ; j ++ ){ + if( ii[i][2][j].length > 0 ){ + all_good = all_good && GraphAnalyzer.dConnected( g, + [g.getVertex(ii[i][0])], [g.getVertex(ii[i][1])], + ii[i][2][j].slice(1) ) + } + } + } + return all_good + })(), true ) + + assert.equal((function(){ + var g = TestGraphs.findExample("Shrier") + var ii = GraphAnalyzer.listMinimalImplications( g ) + var all_good=true + for( var i = 0 ; i < ii.length ; i ++ ){ + for( var j = 0 ; j < ii[i][2].length ; j ++ ){ + all_good = all_good && !GraphAnalyzer.dConnected( g, + [g.getVertex(ii[i][0])], [g.getVertex(ii[i][1])], ii[i][2][j] ) + } + } + return all_good + })(), true ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "mediat" ); + return imp_2_str( GraphAnalyzer.listBasisImplications( g ) ); + })(), "Y _||_ Z | I, X" ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "mediat" ); + return imp_2_str( GraphAnalyzer.listMinimalImplications( g ) ); + })(), "Y _||_ Z | I, X" ) + + assert.equal((function(){ + var g = TestGraphs.findExample( "mediat" ); + g.getVertex("Z").adjusted_for = true; + return imp_2_str( GraphAnalyzer.listMinimalImplications( g ) ); + })(), "Y _||_ Z | I, X" ) + + assert.equal((function(){ + return imp_2_str( + GraphAnalyzer.listMinimalImplications( TestGraphs.commentator1() ) ); + })(), "X _||_ V | W1, Z1\nX _||_ Z2 | V, W2\nX _||_ Z2 | W1, W2, Z1\nY _||_ V | W1, W2, Z1, Z2\nV _||_ W1\nV _||_ W2\nW1 _||_ Z2 | W2\nW2 _||_ Z1 | W1\nZ1 _||_ Z2 | V, W2\nZ1 _||_ Z2 | V, W1" ) +}); + diff --git a/test/test/tetrad-analyis.js b/test/test/tetrad-analyis.js new file mode 100644 index 0000000..91e6510 --- /dev/null +++ b/test/test/tetrad-analyis.js @@ -0,0 +1,59 @@ + +const { + GraphParser,GraphAnalyzer,GraphTransformer +} = require("../../jslib/dagitty-node.js") +const TestGraphs = require("../test-graphs.js") +const _ = require("underscore") + +const $p = (s) => GraphParser.parseGuess(s) + +QUnit.module( "dagitty" ) + +QUnit.test( "tetrad analysis", function( assert ) { + assert.equal( function(){ + var g = $p("dag { {a b} -> x -> y }") + return GraphAnalyzer.vanishingTetrads( g ).length + }() , 1, "choke point in I/J side" ) + + assert.equal((function(){ + var g = $p("dag { xi1 [u] xi2 [u] xi3 [u] xi1 <-> xi2 <-> xi3 <-> xi1 "+ + " xi1 -> {X1 X2 X3} xi2 -> {X4 X5 X6} xi3 -> {X7 X8 X9} }") + return GraphAnalyzer.vanishingTetrads( g ).length + })(), 162 ) + + assert.equal((function(){ + var g = $p("xi1 U\neta1 U\neta2 U\nY1 1\n Y2 1\nY3 1\n Y4 1\nX1 1\n X2 1\n\nxi1 eta1 eta2 X1 X2\neta1 eta2 Y1 Y2\neta2 Y3 Y4") + return ""+GraphAnalyzer.vanishingTetrads( g ).join("\n") + })(), "Y1,Y3,Y4,Y2\nY1,Y3,X1,Y2\nY1,Y3,X2,Y2\nY1,Y4,X1,Y2\nY1,Y4,X2,Y2\nY1,X1,X2,Y2\nY1,Y3,Y4,X1\nY1,Y3,Y4,X2\nY1,X1,X2,Y3\nY1,X1,X2,Y4\nY2,Y3,Y4,X1\nY2,Y3,Y4,X2\nY2,X1,X2,Y3\nY2,X1,X2,Y4\nY3,X1,X2,Y4" ) + + assert.equal((function(){ + var g = $p("xi1 U\nxi2 U\nxi3 U\nU1 U\nU2 U\nU3 U\nX1 1\nX2 1\nX3 1\nX4 1\nX5 1\nX6 1\nX7 1\nX8 1\nX9 1\n\nxi1 X1 X2 X3\nxi2 X4 X5 X6\nxi3 X7 X8 X9\nU1 xi1 xi2\nU2 xi2 xi3\nU3 xi1 xi3") + return GraphAnalyzer.vanishingTetrads( g ).length + })(), 162 ) + + assert.equal((function(){ + var g = $p("xi1 U\neta1 U\neta2 U\nY1 1\n Y2 1\nY3 1\n Y4 1\nX1 1\n X2 1\n\nxi1 eta1 eta2 X1 X2\neta1 eta2 Y1 Y2\neta2 Y3 Y4") + return GraphAnalyzer.vanishingTetrads( g ).length + })(), 15 ) + + assert.equal((function(){ + var g = $p("U U\nX1 1\nX10 1 \nX2 1 \nX3 1 \nX4 1 \nX5 1 \nX6 1 \nX7 1 \nX8 1 \nX9 1 \nxi_1 U \nxi_2 U \n\n"+ + "U xi_1 xi_2\nxi_1 X1 X2 X3 X4 X5\nxi_2 X6 X7 X8 X9 X10\nUa X1 X2\nUb X1 X6") + return ""+GraphAnalyzer.vanishingTetrads( g ).length + })(), 430 ) + + assert.equal((function(){ + var g = $p("u U\nx U\ny U\nx1 1\nx2 1\nx3 1\nx4 1\ny1 1\ny2 1\ny3 1\ny4 1\n\n" + +"u x y\nx x1 x2 x3 x4\ny y1 y2 y3 y4") + return GraphAnalyzer.vanishingTetrads( g ).length + })(), 138 ) + + assert.equal((function(){ + return GraphAnalyzer.vanishingTetrads( TestGraphs.findExample( "Thoemmes" ) ).length + })(), 98 ) + + assert.equal((function(){ + return GraphTransformer.trekGraph( $p("dag {E<-A->Z<-B->D<-E}") ).toAdjacencyList() + })(), "dw_A dw_E dw_Z\ndw_B dw_D dw_Z\ndw_E dw_D\nup_A dw_A\nup_B dw_B\nup_D dw_D up_B up_E\nup_E dw_E up_A\nup_Z dw_Z up_A up_B" ) + +}); diff --git a/test/test/tree-id.js b/test/test/tree-id.js new file mode 100644 index 0000000..e49e8a6 --- /dev/null +++ b/test/test/tree-id.js @@ -0,0 +1,64 @@ + +const { Graph, GraphParser, GraphAnalyzer } = require("../../jslib/dagitty-node.js") + +const $p = (s) => GraphParser.parseGuess(s) + +QUnit.module( "dagitty" ) + +QUnit.test( "treeID", function( assert ) { + //instrument + var r = GraphAnalyzer.treeID($p( "dag { Z -> X \n X -> Y \n X <-> Y }" )).results + assert.equal(r["X"][0].instrument, "Z") + assert.equal(r["X"][0].fastp.length, 1) + assert.equal(r["Y"][0].instrument, "Z") + assert.equal(r["Y"][0].fastp.length, 1) + + //instrument + propagate + var r = GraphAnalyzer.treeID($p( "dag { A -> E \n A <-> D \n E -> D \n E -> v1 }" )).results + assert.equal(r["E"][0].instrument, "A") + assert.equal(r["E"][0].fastp.length, 1) + assert.equal(r["v1"][0].instrument, "A") + assert.equal(r["v1"][0].fastp.length, 1) + assert.equal(r["D"][0].propagate, "E") + assert.equal(r["D"][0].fastp.length, 1) + + //missing cycles (example from weihs/drton tsID) + var r = GraphAnalyzer.treeID($p( "dag { 0 -> 1 \n 0 -> 2 \n 0 -> 3 \n 0 <-> 1 \n 0 <-> 2 \n 0 <-> 3 \n 0 <-> 4 \n 3 -> 4 }" )).results + assert.equal(r["1"][0].propagate, "2") + assert.equal(r["1"][0].fastp.length, 1) + assert.equal(r["2"][0].propagate, "4") + assert.equal(r["2"][0].fastp.length, 1) + assert.equal(r["4"][0].propagate, "3") + assert.equal(r["4"][0].fastp.length, 1) + assert.equal("missingCycles" in r["3"][0], true) + assert.equal(r["3"][0].fastp.length, 1) + + //missing cycles (example (4680, 403) from weihs/drton tsID) + var r = GraphAnalyzer.treeID($p( "dag { 3->4 \n 1->2 \n 0->1 \n 2->3 \n 3<->1 \n 3<->0 \n 1<->0 \n 0<->2 \n 0<->4 }" )).results + assert.equal(r["3"][0].propagate, "4") + assert.equal(r["3"][0].fastp.length, 1) + assert.equal(r["4"][0].propagate, "1") + assert.equal(r["4"][0].fastp.length, 1) + assert.equal(r["1"][0].propagate, "2") + assert.equal(r["1"][0].fastp.length, 1) + assert.equal("missingCycles" in r["2"][0], true) + assert.equal(r["2"][0].fastp.length, 1) + + //not identifiable + var r = GraphAnalyzer.treeID($p( "dag { A -> E \n A <-> D \n A <-> E \n A <-> v1 \n D <-> v1 \n E -> D \n E -> v1 }" )).results + assert.equal( ("A" in r) || ("E" in r) || ("D" in r) || ("v1" in r), false ) + + //2-id, missing cycle [[1, 2], [2, 3], [3, 4], [1, 4]] + var r = GraphAnalyzer.treeID($p( "dag { 0 -> 1 \n 0 <-> 1 \n 0 <-> 2 \n 0 <-> 3 \n 0 <-> 4 \n 1 -> 2 \n 1 <-> 3 \n 2 -> 3 \n 2 <-> 4 \n 3 -> 4 }" )).results + assert.equal(r["4"][0].propagate, "3") + assert.equal(r["4"][0].fastp.length, 2) + assert.equal(r["3"][0].propagate, "2") + assert.equal(r["3"][0].fastp.length, 2) + assert.equal(r["2"][0].propagate, "1") + assert.equal(r["2"][0].fastp.length, 2) + assert.equal("missingCycles" in r["1"][0], true) + assert.equal(r["1"][0].fastp.length, 2) + + +}) + diff --git a/test/tests.js b/test/tests.js index 9c0a911..2bc01f4 100644 --- a/test/tests.js +++ b/test/tests.js @@ -4,1955 +4,8 @@ var $p = function(s){ return GraphParser.parseGuess(s) } var $es = function(g){ return GraphSerializer.toDotEdgeStatements(g) } -QUnit.test( "multivariate polynomials", function( assert ) { - assert.equal(MPoly("0"), "0") - assert.equal(MPoly("1"), "1") - assert.equal(MPoly("2"), "2") - assert.equal(MPoly("-1"), "-1") - assert.equal(MPoly("-2"), "-2") - assert.equal(MPoly("1.234E8"), "123400000") - assert.equal(MPoly("-1.234e8"), "-123400000") - assert.equal(MPoly("a"), "a") - assert.equal(MPoly("b"), "b") - assert.equal(MPoly("abc123"), "abc123") - assert.equal(MPoly("x"), "x") - assert.equal(MPoly("x^2"), "x^2") - assert.equal(MPoly("x^2 y^3"), "x^2 y^3") - assert.equal(MPoly("y^3 x^2"), "x^2 y^3") - assert.equal(MPoly("x x"), "x^2") - assert.equal(MPoly("x x * x * x"), "x^4") - assert.equal(MPoly("y x^2"), "x^2 y") - assert.equal(MPoly("y x^2 z"), "x^2 y z") - assert.equal(MPoly("3x"), "3 x") - assert.equal(MPoly(" 3 x"), "3 x") - assert.equal(MPoly(" 3e2 x"), "300 x") - assert.equal(MPoly(" 3e2 * x"), "300 x") - assert.equal(MPoly(" 3e2 x y"), "300 x y") - assert.equal(MPoly(" 3e2 x^5 y^6"), "300 x^5 y^6") - assert.equal(MPoly("-x^2"), "-x^2") - assert.equal(MPoly("- x^2"), "-x^2") - assert.equal(MPoly("x + y"), "x + y") - assert.equal(MPoly("x + 2y"), "x + 2 y") - assert.equal(MPoly("x+2y"), "x + 2 y") - assert.equal(MPoly("a^2 b^2 + 0x"), "a^2 b^2") - assert.equal(MPoly("a^2 b^2 + 123x"), "a^2 b^2 + 123 x") - assert.equal(MPoly("-2y"), "-2 y") - assert.equal(MPoly("x-2y"), "x - 2 y") - assert.equal(MPoly("x^4-2y^5"), "x^4 - 2 y^5") - assert.equal(MPoly("x^4-2y^5-z^7"), "x^4 - 2 y^5 - z^7") - assert.equal(MPoly("xy + abc123"), "abc123 + xy") - assert.equal(MPoly("xy*abc*def"), "abc def xy") - assert.equal(MPoly("x").negate(), "-x") - assert.equal(MPoly("x+y").negate(), "-x - y") - assert.equal(MPoly("x + 4 y").negate(), "-x - 4 y") - - assert.equal(MPoly("x").add(MPoly("y")), "x + y") - assert.equal(MPoly("x + y").add(MPoly("y + z")), "x + 2 y + z") - assert.equal(MPoly("x + y").add(MPoly("-y + z")), "x + z") - assert.equal(MPoly("x - y - z").add(MPoly("y - z")), "x - 2 z") - assert.equal(MPoly("z").add(MPoly("a + x")), "a + x + z") - assert.equal(MPoly.add(), "0") - assert.equal(MPoly.add(MPoly("x")), "x") - assert.equal(MPoly.add(MPoly("x"), MPoly("y")), "x + y") - assert.equal(MPoly.add(MPoly("x"), MPoly("y"), MPoly("x")), "2 x + y") - assert.equal(MPoly("100 y^3").mul(MPoly("217")), "21700 y^3") - assert.equal(MPoly("100 y^3").mul(MPoly("2 x^100 z^7 zz^8")), "200 x^100 y^3 z^7 zz^8") - assert.equal(MPoly("100 y^3 zzzz^9").mul(MPoly("2 x^100 y z^7 zz^8")), "200 x^100 y^4 z^7 zz^8 zzzz^9") - assert.equal(MPoly("x + y").mul(MPoly("z")), "x z + y z") - assert.equal(MPoly("x + y").mul(MPoly("x y")), "x y^2 + x^2 y") - assert.equal(MPoly("x + y").mul(MPoly("x + y")), "2 x y + x^2 + y^2") - assert.equal(MPoly("x + y").mul(MPoly("0 x")), "0") - assert.equal(MPoly("- x").mul(MPoly("- x")), "x^2") - assert.equal(MPoly("0").isZero(), true) - assert.equal(MPoly("1").isZero(), false) - assert.equal(MPoly("x + y + z").isZero(), false) - assert.equal(MPoly("0 x + 0 y + 0*z").isZero(), true) - - assert.equal(MPoly("x+y").sub(MPoly("x")), "y") - assert.equal(MPoly("x+y").sub(MPoly("y")), "x") - assert.equal(MPoly("x+y").sub(MPoly("x+y")), "0") - assert.equal(MPoly("x").sub(MPoly("x")).isZero(), true) - assert.equal(MPoly("x").sub(MPoly("x + y")).isZero(), false) - - - assert.equal(MPoly("0").eval({"x": MPoly("7")}), "0") - assert.equal(MPoly("x").eval({"x": MPoly("7")}), "7") - assert.equal(MPoly("x").eval({"x": MPoly("y")}), "y") - assert.equal(MPoly("x").eval({"x": MPoly("x + y")}), "x + y") - assert.equal(MPoly("x^2").eval({"x": MPoly("x + y")}), "2 x y + x^2 + y^2") - assert.equal(MPoly("x y").eval({"x": MPoly("y")}), "y^2") - assert.equal(MPoly("x y").eval({"x": MPoly("x + y")}), "x y + y^2") - assert.equal(MPoly("x^2 y").eval({"x": MPoly("x + y")}), "2 x y^2 + x^2 y + y^3") - assert.equal(MPoly("x y + z + a").eval({"x": MPoly("y")}), "a + y^2 + z") - assert.equal(MPoly("x y + z + a").eval({"x": MPoly("x + y")}), "a + x y + y^2 + z") - assert.equal(MPoly("x^2 y + z + a").eval({"x": MPoly("x + y")}), "a + 2 x y^2 + x^2 y + y^3 + z") - assert.equal(MPoly("x^2 y + z + a").eval({"x": MPoly("2 x + 10 y")}), "a + 40 x y^2 + 4 x^2 y + 100 y^3 + z") - assert.equal(MPoly("x^2 y + z + a").eval({"x": MPoly("x + y"), "z": MPoly("3 a^2"), "a": MPoly("b^7")}), "3 a^2 + b^7 + 2 x y^2 + x^2 y + y^3") - assert.equal(MPoly("x^3").eval({"x": MPoly("x + y")}), "3 x y^2 + 3 x^2 y + x^3 + y^3") - assert.equal(MPoly("x^3").eval({"x": MPoly("2 x + 10 y")}), "600 x y^2 + 120 x^2 y + 8 x^3 + 1000 y^3") - assert.equal(MPoly("x^4").eval({"x": MPoly("x + y")}), "4 x y^3 + 6 x^2 y^2 + 4 x^3 y + x^4 + y^4") - assert.equal(MPoly("x^4").eval({"x": MPoly("2 x + 10 y")}), "8000 x y^3 + 2400 x^2 y^2 + 320 x^3 y + 16 x^4 + 10000 y^4") - - - assert.equal(MPoly("0").evalToNumber({"x": 7}), 0) - assert.equal(MPoly("123").evalToNumber({"x": 7}), 123) - assert.equal(MPoly("x").evalToNumber({"x": 7}), 7) - assert.equal(MPoly("x^2").evalToNumber({"x": 7}), 49) - assert.equal(MPoly("x y").evalToNumber({"x": 7, "y": 3}), 21) - assert.equal(MPoly("x y + z + a").evalToNumber({"x": 7, "y": 2, "z": 1000, "a": 200}), 1214) - assert.equal(MPoly("x^3").evalToNumber({"x": 3}), 27) - assert.equal(MPoly("x^4").evalToNumber({"x": 3}), 81) - - assert.equal(MPoly("0").evalToBigInt({"x": 7n}), 0) - assert.equal(MPoly("123").evalToBigInt({"x": 7n}), 123) - assert.equal(MPoly("x").evalToBigInt({"x": 7n}), 7) - assert.equal(MPoly("x^2").evalToBigInt({"x": 7n}), 49) - assert.equal(MPoly("x y").evalToBigInt({"x": 7n, "y": 3n}), 21) - assert.equal(MPoly("x y + z + a").evalToBigInt({"x": 7n, "y": 2n, "z": 1000n, "a": 200n}), 1214) - assert.equal(MPoly("x^3").evalToBigInt({"x": 3n}), 27) - assert.equal(MPoly("x^4").evalToBigInt({"x": 3n}), 81) -}) - - -QUnit.test( "graph manipulation", function( assert ) { - - var g = $p( "dag G { x <-> x }" ) - assert.equal( g.areAdjacent("x","x"), true, "self loop" ) - - g = $p( "map <-> pop" ) - g.changeEdge( g.getEdge("map","pop",Graph.Edgetype.Bidirected), Graph.Edgetype.Directed ) - assert.equal( $es( g ), "map -> pop", "reserved words" ) - - g = $p( "dag G { x <-> y }" ) - assert.equal( g.areAdjacent("x","y"), true ) - assert.equal( g.areAdjacent("y","x"), true ) - g.changeEdge( g.getEdge("x","y",Graph.Edgetype.Bidirected), Graph.Edgetype.Directed ) - assert.equal( $es( g ), "x -> y" ) - assert.equal( g.areAdjacent("x","y"), true ) - assert.equal( g.areAdjacent("y","x"), true ) - - g = $p( "digraph G { x -> y }" ) - assert.equal( g.areAdjacent("x","y"), true ) - g.changeEdge( g.getEdge("x","y"), Graph.Edgetype.Undirected ) - assert.equal( $es( g ), "x -- y" ) - assert.equal( g.areAdjacent("x","y"), true ) - - - g = $p( "digraph G { x -> y }" ) - assert.equal( g.areAdjacent("x","y"), true ) - g.reverseEdge( g.getEdge("x","y"), Graph.Edgetype.Undirected ) - assert.equal( $es( g ), "y -> x" ) - assert.equal( g.areAdjacent("x","y"), true ) - - - g = $p( "digraph G { x -- y }" ) - assert.equal( g.areAdjacent("x","y"), true ) - g.changeEdge( g.getEdge("y","x",Graph.Edgetype.Undirected), Graph.Edgetype.Directed ) - assert.equal( $es( g ), "x -> y" ) - assert.equal( g.areAdjacent("x","y"), true ) - - g = $p( "digraph G { x -- y }" ) - g.changeEdge( g.getEdge("y","x",Graph.Edgetype.Undirected), Graph.Edgetype.Directed, "y" ) - assert.equal( $es( g ), "y -> x", "edge change" ) - - g = $p( "digraph G { y <- x <-> m -- y }") - g.deleteVertex("m") - assert.equal( $es( g ), "x -> y", "vertex deletion" ) - - -} ) - -QUnit.test( "parsing and serializing", function( assert ) { - // GraphParser.VALIDATE_GRAPH_STRUCTURE = false; - // - // - assert.equal( $es( $p( "map -> pop" ) ), "map -> pop", "reserved words" ) - - assert.equal( $es( $p( "x -> y" ) ), "x -> y", "shorthand notation" ) - - assert.equal( $p( "dag{{x->{a b}}}" ).edges.length, 2, "grouping" ) - - assert.equal( $p( "dag{{x->{a ; b}}}" ).edges.length, 2, - "semicolons" ) - assert.equal( $p( "dag{a;b}" ).getVertices().length, 2, - "semicolons 2" ) - assert.equal( $p( "dag{;b}" ).getVertices().length, 1, - "semicolons 2" ) - assert.equal( $p( "dag{a;}" ).getVertices().length, 1, - "semicolons 2" ) - - assert.equal( $p( "dag{}" ).getVertices().length, 0, - "semicolons 2" ) - assert.equal( $p( "dag{;}" ).getVertices().length, 0, - "semicolons 2" ) - assert.equal( $p( "dag{;;;}" ).getVertices().length, 0, - "semicolons 2" ) - - assert.equal( $p( "dag{a}" ).getVertices().length, - 1, "spaces" ) - assert.equal( $p( "dag{a }" ).getVertices().length, - 1, "spaces" ) - assert.equal( $p( "dag{ a }" ).getVertices().length, - 1, "spaces" ) - - - assert.equal( $p( "pag{a @-> {b --@ {c @-@ d} }}" ).edges.length, 6 ) - assert.equal( _.pluck($p( "pag{a @->b<-@c}" ).getVertices(),"id").sort().join(","), "a,b,c" ) - assert.equal( _.pluck($p( "pag{a @->b<-@c @-@d}" ).getVertices(),"id").sort().join(","), "a,b,c,d" ) - - assert.equal( $p( "dag{x->{a b}}" ).edges.length, 2 ) - - assert.equal( $p( "dag{x->{a->b}}" ).edges.length, 3 ) - - assert.equal( $p( "dag{a->{b->{c->{d->e}}}}" ).edges.length, 10 ) - - assert.equal( GraphSerializer.toDotVertexStatements( $p( - "digraph G { \"ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ\" }" ) ).trim(), - "\"ᚠᛇᚻ᛫ᛒᛦᚦ᛫ᚠᚱᚩᚠᚢᚱ᛫ᚠᛁᚱᚪ᛫ᚷᛖᚻᚹᛦᛚᚳᚢᛗ\"" ) - - assert.equal( GraphSerializer.toDotVertexStatements( $p( - "digraph G { x [label=\"από το Άξιον Εστί\"] }" ) ).trim(), - "x [label=\"από το Άξιον Εστί\"]" ) - - assert.equal( GraphSerializer.toDot( $p( - "digraph G { graph [bb=\"0,0,100,100\"] }" ) ).trim(), - "digraph G{\nbb=\"0,0,100,100\"\n\n}" ) - - assert.equal( TestGraphs.small1().oldToString(), "A 1\nS E\nT O\n\nA S\nS T" ) - - assert.equal((function(){ - var g = $p( - "digraph G { x <-> y }" - ).addSource("x").addTarget("y") - GraphTransformer.activeBiasGraph( g ) - return g.edges[0].directed - })(), Graph.Edgetype.Bidirected ) - - assert.equal((function(){ - var g = $p( - "digraph G { xi1 [latent]\n"+ - "xi1 -> x1\n"+ - "xi1 -> x3\n"+ - "xi1 -> x4\n"+ - "xi1 -> x5\n"+ - "x5 -> x6\n"+ - "x1 -> x6\n"+ - "x6 -> x3 }\n" - ) - return g.oldToString() - })(), "x1 1\nx3 1\nx4 1\nx5 1\nx6 1\nxi1 U\n\nx1 x6\nx5 x6\nx6 x3\nxi1 x1 x3 x4 x5" ) - - assert.equal((function(){ - var g = GraphParser.parseDot( - "digraph { xi1 [latent]\n"+ - "xi2 [latent]\n"+ - "xi1 <-> xi2\n"+ - "xi1 -> x1\n"+ - "xi1 -> x2\n"+ - "xi1 -> x3\n"+ - "xi2 -> x4\n"+ - "xi2 -> x5\n"+ - "xi2 -> x6 } " - ) - return g.oldToString() - })(), "x1 1\nx2 1\nx3 1\nx4 1\nx5 1\nx6 1\nxi1 U\nxi2 U\n\nxi1 x1 x2 x3 xi2\nxi2 x4 x5 x6 xi1" ) - - assert.equal((function(){ - var g = GraphParser.parseDot( "digraph { x -> y\ny -> z\nx <-> y } " ) - //console.log( g.toString() ) - return $es(g) - })(), "x -> y\nx <-> y\ny -> z" ) - - - assert.equal((function(){ - var g = $p( - "digraph G {\ny -- x -> y <-> x }" ) - return $es(g) - })(), "x -- y\nx -> y\nx <-> y" ) - - - assert.equal((function(){ - var g = $p( - "graph G {\nx [ exposure , pos =\" 12. , .13 \"]\ny [outcome]\n}" ) - return GraphSerializer.toDot(g) - })(), "graph G{\nx [exposure,pos=\"12.000,0.130\"]\ny [outcome]\n\n}\n" ) - - - assert.equal((function(){ - var g = $p( - "graph G { x [exposure] \n y [outcome] }" ) - g.getVertex("x").layout_pos_x = 1.0 - g.getVertex("x").layout_pos_y = 1.0 - return GraphSerializer.toDot(g) - })(), "graph G{\nx [exposure,pos=\"1.000,1.000\"]\ny [outcome]\n\n}\n" ) - - assert.equal((function(){ - var g = $p( - "graph G { x \n y }" ) - return GraphSerializer.toDot(g) - })(), "graph G{\nx\ny\n\n}\n" ) - - assert.equal((function(){ - var g = $p( - "graph G { x [] \n y [] }" ) - return GraphSerializer.toDot(g) - })(), "graph G{\nx\ny\n\n}\n" ) - - assert.equal((function(){ - var g = $p( - "digraph G { x [] \n y [] }" ) - g.addEdge("x","y",Graph.Edgetype.Directed) - g.addEdge("x","y",Graph.Edgetype.Undirected) - g.addEdge("x","y",Graph.Edgetype.Bidirected) - return $es(g) - })(), "x -- y\nx -> y\nx <-> y" ) - - assert.equal((function(){ - var g = $p( "digraph G { x <-> m } " ) - return typeof g.getEdge("m","x",2) - })(), "object" ) - - assert.equal((function(){ - var g = $p( "digraph G { x <-> m } " ) - return typeof g.getEdge("x","m",2) - })(), "object" ) - - assert.equal((function(){ - var g = $p( "digraph G { x -> m } " ) - return $es(g) - })(), "x -> m" ) - - assert.equal((function(){ - var g = $p( "dag G { M [pos=\"-0.521,-0.265\"] \n "+ - "X [exposure,pos=\"-1.749,-0.238\"] \n "+ - "Y [outcome,pos=\"1.029,-0.228\"] \n "+ - "M <-> Y [pos=\"0.645,-0.279\"] \n "+ - "X -> Y \n "+ - "X -> M -> Y } " ) - return GraphSerializer.toDot(g) - })(), "dag G{\nM [pos=\"-0.521,-0.265\"]\n"+ - "X [exposure,pos=\"-1.749,-0.238\"]\n"+ - "Y [outcome,pos=\"1.029,-0.228\"]\n"+ - "M -> Y\n"+ - "M <-> Y [pos=\"0.645,-0.279\"]\n"+ - "X -> M\n"+ - "X -> Y\n}\n" ) - - assert.equal((function(){ - var g = $p( "dag G { M [pos=\"-0.521,-0.265\"] "+ - "X [exposure,pos=\"-1.749,-0.238\"] "+ - "Y [outcome,pos=\"1.029,-0.228\"] "+ - "M <-> Y [pos=\"0.645,-0.279\"] "+ - "X -> Y "+ - "X -> M -> Y } " ) - return GraphSerializer.toDot(g) - })(), "dag G{\nM [pos=\"-0.521,-0.265\"]\n"+ - "X [exposure,pos=\"-1.749,-0.238\"]\n"+ - "Y [outcome,pos=\"1.029,-0.228\"]\n"+ - "M -> Y\n"+ - "M <-> Y [pos=\"0.645,-0.279\"]\n"+ - "X -> M\n"+ - "X -> Y\n}\n" ) - - assert.equal((function(){ - var g = $p( - "digraph G { x -- y \n x -> y \n x <-> y [pos=\"1.000,1.000\"] }" ) - return $es(g) - })(), "x -- y\nx -> y\nx <-> y [pos=\"1.000,1.000\"]" ) - - assert.equal((function(){ - var g = $p( - "digraph G {\n y -- x -> y <-> x [pos=\"1.0,0.1\"] }" ) - return $es(g) - })(), "x -- y [pos=\"1.000,0.100\"]\nx -> y [pos=\"1.000,0.100\"]\nx <-> y [pos=\"1.000,0.100\"]" ) - - assert.equal(GraphSerializer.toLavaan($p("dag{x1}")).split("\n")[1] - ,"x1 ~~ x1") - - assert.equal( $es($p( - "dag{ U -> {a b c d} }" )), "U -> a\nU -> b\nU -> c\nU -> d" ) - - -}); - -QUnit.test( "ancestry", function( assert ) { - var shrier = TestGraphs.findExample("Shrier") - assert.equal( - (_.pluck(shrier.ancestorsOf( - shrier.getVertex(["PreGameProprioception"])),"id")).sort().join(",") - , "Coach,FitnessLevel,Genetics,PreGameProprioception" ) - - assert.equal((function(){ - var g = $p( - "digraph G {a -- b }" ) - return g.getVertex("a").getNeighbours().length - })(), 1 ) - -}); - -QUnit.test( "separators", function( assert ) { - var g, gm - function verts(a) { - return _.isArray(a) ? a.map(function(vid){return g.getVertex(vid)}) : [g.getVertex(a)] - } - - - g = $p("length -> push -> pop map -> { length push }") - assert.equal( GraphAnalyzer.listMinimalImplications( g ).length, 2, "reserverd words" ) - - g = GraphTransformer.backDoorGraph(TestGraphs.small1()) - assert.equal( sep_2_str( GraphAnalyzer.listMinimalSeparators(g) ), "{}" ) - assert.strictEqual( sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g) ] ) , "{}" ) - - g = GraphTransformer.backDoorGraph(TestGraphs.confounding_triangle()) - gm = GraphTransformer.moralGraph(GraphTransformer.ancestorGraph(g)) - assert.equal( sep_2_str( GraphAnalyzer.listMinimalSeparators(gm) ), "{C}" ) - assert.equal( sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g) ] ), "{C}" ) - - - g = GraphTransformer.backDoorGraph(TestGraphs.m_bias_graph()) - gm = GraphTransformer.moralGraph(GraphTransformer.ancestorGraph(g)) - assert.equal( sep_2_str ( GraphAnalyzer.listMinimalSeparators(gm) ), "{}" ) - assert.equal( sep_2_str( [ GraphAnalyzer.findMinimalSeparator(gm) ] ) , "{}" ) - - g = GraphTransformer.backDoorGraph(TestGraphs.extended_confounding_triangle()) - gm = GraphTransformer.moralGraph(GraphTransformer.ancestorGraph(g)) - assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm) ), "{C, D}\n{C, E}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g) ] ) , "{C, D}") - - assert.equal(sep_2_str(GraphAnalyzer.listMinimalSeparators( gm, verts(["D"]), [] ) ), "{C, D}" ) - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, verts(["D"]), []) ] ) , "{C, D}") - - assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm, [], [], 1) ), "{C, E}") - assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm, [], verts(["D"])) ), "{C, E}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["D"]) ) ] ) , "{C, E}") - - g = $p("digraph {"+ - "D [outcome] "+ - "E [exposure] "+ - "v1 v2 v3 v4 v5 v6 v7 "+ - "D <-> v5 "+ - "E -> v1 "+ - "E -> v6 "+ - "v1 -> D "+ - "v2 -> D "+ - "v2 -> E "+ - "v3 -> E "+ - "v3 <-> v4 "+ - "v4 -> v6 "+ - "v5 -> v4 "+ - "v6 -> v7 "+ - "v7 -> D"+ - "}") - - gm = GraphTransformer.moralGraph(GraphTransformer.ancestorGraph(g)) - assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm) ), "{v1, v2, v3, v4, v6}\n{v1, v2, v3, v4, v7}\n{v1, v2, v5, v6}\n{v1, v2, v5, v7}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g) ] ), "{v1, v2, v3, v4, v6}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("D"), verts("E")) ] ), "{v1, v2, v5, v7}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, verts("v7")) ] ), "{v1, v2, v3, v4, v7}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, verts("v5")) ] ), "{v1, v2, v5, v6}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, verts(["v5","v7"])) ] ), "{v1, v2, v5, v7}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v7"])) ] ), "{v1, v2, v3, v4, v6}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v6"])) ] ), "{v1, v2, v3, v4, v7}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v4"])) ] ), "{v1, v2, v5, v6}") - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v4","v6"])) ] ), "{v1, v2, v5, v7}") - assert.strictEqual(GraphAnalyzer.findMinimalSeparator(g, null, null, [], verts(["v2"])) , false) - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("v4"), verts("v2")) ]) , "{}" ) - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("v4"), verts("v2"), verts("D"), []) ]) , "{D, E, v3, v5, v6}" ) - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("v4"), verts("v2"), verts(["D","v7"]), []) ]) , "{D, E, v3, v5, v7}" ) - assert.equal(sep_2_str( [ GraphAnalyzer.findMinimalSeparator(g, verts("v4"), verts("v2"), verts("D"), verts("v6")) ]) , "{D, E, v3, v5, v7}" ) - - gm.setSources(verts("v4")) - gm.setTargets(verts("v2")) - assert.equal(sep_2_str( GraphAnalyzer.listMinimalSeparators(gm, verts("D"), []) ) , "{D, E, v3, v5, v6}\n{D, E, v3, v5, v7}" ) -} ); - -QUnit.test( "graph analysis", function( assert ) { - var g = $p("dag{x->y}") - assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) - g = $p("mag{x->y}") - assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),false) - g = $p("mag{z<->x->y}") - assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) - g = $p("mag{z->x->y}") - assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) - g = $p("mag{z--x->y}") - assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),false) - g = $p("mag{c<->a {a<->b<->x}->y}") - assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) - g = $p("mag{c->a {a<->b<->x}->y}") - assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) - g = $p("mag{c->{a b x}->y}") - assert.equal(GraphAnalyzer.isEdgeVisible(g,g.getEdge("x","y")),true) - - - - - g = $p("dag{x->y}") - assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("x","y")),false) - g = $p("dag{x->y<-z}") - assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("x","y")),true) - g = $p("dag{x->y<-z->x}") - assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("x","y")),false) - g = $p("dag{{a b}->x->y}") - assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("x","y")),true) - - g = $p("dag{x<-z3<-z1->x}") - assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("z1","x")),true) - assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("z3","x")),false) - assert.equal(GraphAnalyzer.isEdgeStronglyProtected(g,g.getEdge("z1","z3")),false) - - assert.equal( - GraphAnalyzer.containsCycle( TestGraphs.cyclic_graph() ), "A→B→C→A" ) - assert.equal( - $p("x E\ny O\nz\na\n\nx y a z\na y\nz y").countPaths(),3) - assert.equal((function(){ - // note, this test is overly restrictive as only one solution is - // considered legal - var g = TestGraphs.extended_confounding_triangle(); - var to = GraphAnalyzer.topologicalOrdering(g) - var vids = _.pluck(g.vertices.values(),"id") - vids.sort( function(a,b){ return to[a] < to[b] ? -1 : 1 } ) - return vids.toString(); - })(),"E,D,C,A,B") - - var chordalGraphs = [ - "graph { t -- x \n x -- y \n y -- t }", - "graph { A -- E \n A -- Z \n B -- D \n B -- Z \n D -- Z \n E -- Z \n }", - "graph { A -- B \n A -- E \n A -- Z \n B -- D \n B -- Z \n D -- Z \n E -- Z \n }", - "graph { A -- B \n A -- E \n A -- Z \n B -- D \n B -- E \n B -- Z \n D -- Z \n E -- Z }" - ]; - _.each(chordalGraphs, function(g) { - assert.equal(GraphAnalyzer.isChordal($p(g)), true); - }); - assert.equal(GraphAnalyzer.isChordal(TestGraphs.K5), true); - - var notChordalGraphs = [ - "graph {a -- b \n a -- x \n b -- y \n x -- y}", - "graph {A -- E \n A -- Z \n B -- Z \n B -- D \n D -- E}", - "graph {A -- E \n B -- D \n B -- Z \n B -- A \n D -- Z \n E -- Z }" - ]; - _.each(notChordalGraphs, function(g) { - assert.equal(GraphAnalyzer.isChordal($p(g)), false); - }); -}); - -QUnit.test( "biasing paths in DAGs (allowing <->)", function( assert ) { - assert.equal((function(){ - var g = TestGraphs.confounding_triangle_with_irrelevant_nodes(); - var vv = []; - _.each(GraphTransformer.activeBiasGraph(g).vertices.values(),function( v ){ - vv.push( v.id ); - }) - vv.sort(); - return vv.join(","); - })(),"A,B,M" ) -assert.equal((function(){ - var g = $p( "digraph G { x <-> y } " ) - return $es( - GraphTransformer.activeBiasGraph(g,g.getVertex(["x"]),g.getVertex(["y"]))) -})(), "x <-> y" ) - -assert.equal((function(){ - var g = $p( "digraph G { x <- m <-> y } " ) - g.addSource("x") - g.addTarget("y") - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "m -> x\nm <-> y" ) - -assert.equal((function(){ - var g = $p( "digraph G { x -> m <-> y } " ) - g.addSource("x") - g.addTarget("y") - g.addAdjustedNode("m") - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "m <-> y\nx -> m" ) - -assert.equal((function(){ - var g = $p( "digraph G { m <-> y \n x -> m -> y } " ) - g.addSource("x") - g.addTarget("y") - g.addAdjustedNode("m") - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "m <-> y\nx -> m" ) - -assert.equal((function(){ - var g = $p( "digraph G { m <-> y \n x -> m -> y } " ) - g.addSource("x") - g.addTarget("y") - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "" ) - -assert.equal((function(){ - var g = $p( "digraph G { M \n "+ - "X [exposure] \n "+ - "Y [outcome] \n "+ - "M <-> Y [pos=\"0.645,-0.279\"] \n "+ - "X -> Y \n "+ - "X -> M -> Y } " ) - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "" ) - -}); - -QUnit.test( "graph transformations", function( assert ) { - assert.equal( - $es(GraphTransformer.lineDigraph( - $p( "dag{a<-b->c}" ))), - "") - assert.equal( - $es(GraphTransformer.lineDigraph( - $p( "dag{a->b->c}" ))), - "ab -> bc") - - assert.equal( - $es(GraphTransformer.lineDigraph( - $p( "dag{a->b [label=x] b->c [label=y]}" ))), - "x -> y") - - assert.equal( - GraphTransformer.ancestorGraph( TestGraphs.small1() ).oldToString(), - "A 1\nS E\nT O\n\nA S\nS T" ) - assert.equal( - GraphTransformer.moralGraph( - GraphTransformer.ancestorGraph(TestGraphs.small1()) ).oldToString() - , "A 1\nS E\nT O\n\nA S\nS A T\nT S" ) - assert.equal((function(){ - var g = TestGraphs.confounding_triangle_with_irrelevant_nodes(); - var vv = []; - _.each(GraphTransformer.ancestorGraph(g).vertices.values(),function( v ){ - vv.push( v.id ); - } ); - vv.sort(); - return vv.join(","); - })(), "A,B,M,U1,U2" ) - assert.equal((function(){ - var g = TestGraphs.m_bias_graph(); - g.addAdjustedNode( "M" ); - return GraphTransformer.moralGraph( - GraphTransformer.ancestorGraph(GraphTransformer.backDoorGraph(g)) ).oldToString() - })(), "A E\nB O\nM A\nU1 1\nU2 1\n\n"+ - "A U1\nB U2\nM U1 U2\nU1 A M U2\nU2 B M U1" ) - - assert.equal((function(){ - return GraphTransformer.moralGraph( - GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph(TestGraphs.m_bias_graph())) ).oldToString() - })(), "A E\nB O\nU1 1\nU2 1\n\nA U1\nB U2\nU1 A\nU2 B" ) - - assert.equal((function(){ - var g = $p("dag{x<-z3<-z1->x}") - return GraphTransformer.markovEquivalentDags(g).length - })(),6,"markov equiv") - - var transformations = [ - GraphTransformer.pagToPdag, - "pag{x@-@y@->z}","pdag{x--y->z}", - - GraphTransformer.dagToCpdag, - "dag{x->y}", "pdag { x -- y }", - "dag{x->y<-z}", "pdag { x -> y <- z }", - "dag{x->y<-z->x}", "pdag { x -- y -- z -- x }", - "dag{z1->{x z3} z2->{z3 y} z3->{x y} x->w->y}", - "pdag {x <- z1 z1->z3 z2->z3 y<-z2 x<-z3 z3->y x->w->y}", - - GraphTransformer.cgToRcg, - "pdag { k -- n; l -- m; n -- t; t -- y; x -> y; }", - "pdag { k;l;m;n;t;x;y; l -- m; n -> k; t -> n; x -> y; y -> t }", - "pdag { k -- n; l -- m; l -> n; l -> t; l -> y; n -- t; t -- y; x -> y; }", - "pdag { k;l;m;n;t;x;y; l -- m; l -> n; l -> t; l -> y; n -> k; t -> n; x -> y; y -> t }", - "pdag {a -> b -- c <- d }", - null, - "digraph { a -> b; b -- c; c <- d; b -- d }", - "digraph { a -> b; b -> c; b -> d; d -> c }", - - function (g){ return GraphTransformer.contractComponents(g, - GraphAnalyzer.connectedComponents(g), [Graph.Edgetype.Directed])}, - "pdag { k -- n l -- m n -- t t -- y x -> y }", - 'pdag { "l,m" ; x -> "k,n,t,y" }', - "digraph { a -- b; b -> c; c -- a }", - 'digraph { "a,b,c" -> "a,b,c" }', - - GraphTransformer.transitiveClosure, - "dag G { x -> y -> z }", - "dag G { x -> y -> z <- x }", - - GraphTransformer.transitiveReduction, - "dag G { x -> y -> z <- x }", - "dag G { x -> y -> z }", - - GraphTransformer.dag2DependencyGraph, - "dag G { x -> y -> z <- x }", - "graph G { x -- y -- z -- x }", - "dag G { x -> y -> z }", - "graph G { x -- y -- z -- x }", - "dag G { x -> y <- z }", - "graph G { x -- y -- z }", - - GraphTransformer.dagToMag, - "dag { v1 [latent] z <- v1 -> v2 -> y x -> v1}", - "mag { x -> z <-> v2 -> y x -> v2 }", - "dag { l1 [latent] l2 [latent] a -> x l1 -> x l1 -> z l2 -> y l2 -> z x -> y z -> x z -> y}", - "mag { a -> x a -> y x -> y z -> x z -> y } ", - - ]; - var i = 0; var transfunc; - while (i < transformations.length) { - if (typeof transformations[i] === "function") { - transfunc = transformations[i]; - i ++; - } - var gin = transformations[i]; i++; - if (typeof gin === "string") gin = $p(gin); - var gout = transfunc(gin); - var gref = transformations[i]; i++; - if( gref != null ){ gref = $p(gref) } - assert.equal(GraphAnalyzer.equals(gout, gref),true); - } -}); - -QUnit.test( "adjustment in DAGs", function( assert ) { - - assert.equal((function(){ - var g = $p("pdag{ x -> {a -- b -- c -- a } b -> y x[e] y[o] }") - return _.pluck(GraphAnalyzer.properPossibleCausalPaths(g),"id").sort().join(" ") - })(), "a b c x y" ) - - assert.equal((function(){ - var g = $p("pdag{ x -> a -- b -> y x[e] y[o] }") - return _.pluck(GraphAnalyzer.properPossibleCausalPaths(g),"id").sort().join(" ") - })(), "a b x y" ) - - assert.equal((function(){ - var g = $p("pdag{ x -> a -- b -- c -> y x[e] y[o] }") - return _.pluck(GraphAnalyzer.properPossibleCausalPaths(g),"id").sort().join(" ") - })(), "a b c x y" ) - - - assert.equal((function(){ - // our non-X-ancestor MSAS example from the UAI paper w/ causal edge - var g = $p("X1 E\nX2 E\nY O\nM1 1\nM2 1\n\nX1 Y M1\nY M2\nM1 M2\nM2 X2") - return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) - })(), "" ) - - assert.equal((function(){ - // our non-X-ancestor MSAS example from the UAI paper w/o causal edge - var g = $p("X1 E\nX2 E\nY O\nM1 1\nM2 1\n\nX1 M1\nY M2\nM1 M2\nM2 X2") - return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) - })(), "{M1, M2}" ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "Thoemmes" ) - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ) - })(), "" ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "Thoemmes" ) - g.removeAdjustedNode("s2") - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ) - })(), "{}" ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "Thoemmes" ) - return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) - })(), "{e2, s2}\n{s1, s2}" ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "mediat" ) - return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) - })(), "{I}" ) - - assert.equal((function(){ - var g = $p("dag{x[e] y[o] x<-z->y}") - return GraphAnalyzer.isAdjustmentSet(g,["z"]) - })(), true) - - assert.equal((function(){ - var g = $p("dag{x[e] y[o] x<->m<->y x->y}") - return GraphAnalyzer.isAdjustmentSet(g,["m"]) - })(), false) - - assert.equal( - sep_2_str( GraphAnalyzer.listMsasTotalEffect( TestGraphs.findExample("Polzer") ) ), - "{Age, Alcohol, Diabetes, Obesity, Psychosocial, Sex, Smoking, Sport}\n"+ - "{Age, Alcohol, Periodontitis, Psychosocial, Sex, Smoking}" ) - assert.equal((function(){ - var g = TestGraphs.m_bias_graph(); - g.addAdjustedNode("A"); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); - })(), "{A}" ) - assert.equal((function(){ - var g = TestGraphs.m_bias_graph(); - g.addAdjustedNode("M"); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); - })(), "{M, U1}\n{M, U2}" ) - assert.equal((function(){ - var g = TestGraphs.findExample( "Acid" ); - g.addAdjustedNode("x9"); - return GraphAnalyzer.violatesAdjustmentCriterion( g ) - })(), true ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "Acid" ); - g.addAdjustedNode("x16"); - return GraphAnalyzer.violatesAdjustmentCriterion( g ) - })(), true ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "Acid" ); - return GraphAnalyzer.violatesAdjustmentCriterion( g ) - })(), false ) - - assert.equal( sep_2_str(GraphAnalyzer.listMsasTotalEffect( - $p("dag { X1 [exposure]\n "+ - "X2 [exposure]\n Y1 [outcome] \n Y2 [outcome]\n "+ - "C -> Y1 \n"+ - "C -> m \n X1 -> X2 \n X1 -> Y2 -> Y1 \n X2 -> Y1 \n X1 -> m2 -> m -> X2 }"))), - "{C}\n{m, m2}") - - g = $p( "dag G { a <-> b } " ) - assert.equal( GraphTransformer.trekGraph( g ).edges.length, 4, "trek graph with <->" ) - - -}); - -QUnit.test( "adjustment in other graphs", function( assert ) { - assert.equal( sep_2_str(GraphAnalyzer.listMsasTotalEffect( - $p("pdag { X [exposure]\n"+ - "Y [outcome]\n"+ - "X -> W -> Y ; F -> W -- Z ; X <- F -> Z }") - ) ), - "{F}" ) - - assert.equal( sep_2_str(GraphAnalyzer.listMsasTotalEffect( - $p("mag { X [e] Y[o] X->Y }") - ) ), "" ) - - assert.equal( sep_2_str(GraphAnalyzer.listMsasTotalEffect( - $p("mag { X [e] Y[o] I->X->Y }") - ) ), "{}" ) - - assert.equal( GraphAnalyzer.listMsasTotalEffect($p($p("pag{i<-@x->y x[e] y[o]}").toString()) ).length, - 0 ) - -}); - -QUnit.test( "PAGs", function( assert ) { - - assert.equal( GraphAnalyzer.listMsasTotalEffect( - TestGraphs.spirtes, [], [], 200 ).length, 200 ) - - assert.equal( GraphAnalyzer.violatesAdjustmentCriterion( - TestGraphs.spirtes ), false ) - - var gam = GraphTransformer.moralGraph( - GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph( TestGraphs.spirtes ) ) ) - - assert.equal( gam.edges.length, 302 ) - - assert.equal( GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph( - TestGraphs.spirtes ) ).edges.length, 133 ) - - - assert.equal( GraphTransformer.backDoorGraph( - TestGraphs.spirtes ).edges.length, 795 ) - - assert.equal( - GraphTransformer.backDoorGraph( - $p("pag{ {V2 V1} @-> X -> {V4 @-> Y} <- V3 @-> X X[e] Y[o]}")). - getVertex("X").getChildren().length, 0, "No children of X" ) - - assert.equal( - GraphTransformer.backDoorGraph( - $p("pag{ {V2 V1} -> X -> {V4 -> Y} <- V3 -> X X[e] Y[o]}")). - getVertex("X").getChildren().length, 0 ) - - assert.equal( - GraphTransformer.backDoorGraph( - $p("pdag{ {V2 V1} -> X -> {V4 -> Y} <- V3 -> X X[e] Y[o]}")). - getVertex("X").getChildren().length, 0 ) - - assert.equal( - GraphAnalyzer.dpcp($p("pag{ {V2 V1} -> X -> V4 -> Y <- V3 <-> {X V4} X[e] Y[o]}")).length, 2 ) - -}); - -QUnit.test( "testable implications", function( assert ) { - assert.equal((function(){ - var g = TestGraphs.findExample("Shrier") - var ii = GraphAnalyzer.listMinimalImplications( g ) - var all_good=true - for( var i = 0 ; i < ii.length ; i ++ ){ - for( var j = 0 ; j < ii[i][2].length ; j ++ ){ - if( ii[i][2][j].length > 0 ){ - all_good = all_good && GraphAnalyzer.dConnected( g, - [g.getVertex(ii[i][0])], [g.getVertex(ii[i][1])], - ii[i][2][j].slice(1) ) - } - } - } - return all_good - })(), true ) - - assert.equal((function(){ - var g = TestGraphs.findExample("Shrier") - var ii = GraphAnalyzer.listMinimalImplications( g ) - var all_good=true - for( var i = 0 ; i < ii.length ; i ++ ){ - for( var j = 0 ; j < ii[i][2].length ; j ++ ){ - all_good = all_good && !GraphAnalyzer.dConnected( g, - [g.getVertex(ii[i][0])], [g.getVertex(ii[i][1])], ii[i][2][j] ) - } - } - return all_good - })(), true ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "mediat" ); - return imp_2_str( GraphAnalyzer.listBasisImplications( g ) ); - })(), "Y _||_ Z | I, X" ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "mediat" ); - return imp_2_str( GraphAnalyzer.listMinimalImplications( g ) ); - })(), "Y _||_ Z | I, X" ) - - assert.equal((function(){ - var g = TestGraphs.findExample( "mediat" ); - g.getVertex("Z").adjusted_for = true; - return imp_2_str( GraphAnalyzer.listMinimalImplications( g ) ); - })(), "Y _||_ Z | I, X" ) - - assert.equal((function(){ - return imp_2_str( - GraphAnalyzer.listMinimalImplications( TestGraphs.commentator1() ) ); - })(), "X _||_ V | W1, Z1\nX _||_ Z2 | V, W2\nX _||_ Z2 | W1, W2, Z1\nY _||_ V | W1, W2, Z1, Z2\nV _||_ W1\nV _||_ W2\nW1 _||_ Z2 | W2\nW2 _||_ Z1 | W1\nZ1 _||_ Z2 | V, W2\nZ1 _||_ Z2 | V, W1" ) -}); - -QUnit.test( "tetrad analysis", function( assert ) { - assert.equal( function(){ - var g = $p("dag { {a b} -> x -> y }") - return GraphAnalyzer.vanishingTetrads( g ).length - }() , 1, "choke point in I/J side" ) - - assert.equal((function(){ - var g = $p("dag { xi1 [u] xi2 [u] xi3 [u] xi1 <-> xi2 <-> xi3 <-> xi1 "+ - " xi1 -> {X1 X2 X3} xi2 -> {X4 X5 X6} xi3 -> {X7 X8 X9} }") - return GraphAnalyzer.vanishingTetrads( g ).length - })(), 162 ) - - assert.equal((function(){ - var g = $p("xi1 U\neta1 U\neta2 U\nY1 1\n Y2 1\nY3 1\n Y4 1\nX1 1\n X2 1\n\nxi1 eta1 eta2 X1 X2\neta1 eta2 Y1 Y2\neta2 Y3 Y4") - return ""+GraphAnalyzer.vanishingTetrads( g ).join("\n") - })(), "Y1,Y3,Y4,Y2\nY1,Y3,X1,Y2\nY1,Y3,X2,Y2\nY1,Y4,X1,Y2\nY1,Y4,X2,Y2\nY1,X1,X2,Y2\nY1,Y3,Y4,X1\nY1,Y3,Y4,X2\nY1,X1,X2,Y3\nY1,X1,X2,Y4\nY2,Y3,Y4,X1\nY2,Y3,Y4,X2\nY2,X1,X2,Y3\nY2,X1,X2,Y4\nY3,X1,X2,Y4" ) - - assert.equal((function(){ - var g = $p("xi1 U\nxi2 U\nxi3 U\nU1 U\nU2 U\nU3 U\nX1 1\nX2 1\nX3 1\nX4 1\nX5 1\nX6 1\nX7 1\nX8 1\nX9 1\n\nxi1 X1 X2 X3\nxi2 X4 X5 X6\nxi3 X7 X8 X9\nU1 xi1 xi2\nU2 xi2 xi3\nU3 xi1 xi3") - return GraphAnalyzer.vanishingTetrads( g ).length - })(), 162 ) - - assert.equal((function(){ - var g = $p("xi1 U\neta1 U\neta2 U\nY1 1\n Y2 1\nY3 1\n Y4 1\nX1 1\n X2 1\n\nxi1 eta1 eta2 X1 X2\neta1 eta2 Y1 Y2\neta2 Y3 Y4") - return GraphAnalyzer.vanishingTetrads( g ).length - })(), 15 ) - - assert.equal((function(){ - var g = $p("U U\nX1 1\nX10 1 \nX2 1 \nX3 1 \nX4 1 \nX5 1 \nX6 1 \nX7 1 \nX8 1 \nX9 1 \nxi_1 U \nxi_2 U \n\n"+ - "U xi_1 xi_2\nxi_1 X1 X2 X3 X4 X5\nxi_2 X6 X7 X8 X9 X10\nUa X1 X2\nUb X1 X6") - return ""+GraphAnalyzer.vanishingTetrads( g ).length - })(), 430 ) - - assert.equal((function(){ - var g = $p("u U\nx U\ny U\nx1 1\nx2 1\nx3 1\nx4 1\ny1 1\ny2 1\ny3 1\ny4 1\n\n" - +"u x y\nx x1 x2 x3 x4\ny y1 y2 y3 y4") - return GraphAnalyzer.vanishingTetrads( g ).length - })(), 138 ) - - assert.equal((function(){ - return GraphAnalyzer.vanishingTetrads( TestGraphs.findExample( "Thoemmes" ) ).length - })(), 98 ) - - assert.equal((function(){ - return GraphTransformer.trekGraph( $p("dag {E<-A->Z<-B->D<-E}") ).toAdjacencyList() - })(), "dw_A dw_E dw_Z\ndw_B dw_D dw_Z\ndw_E dw_D\nup_A dw_A\nup_B dw_B\nup_D dw_D up_B up_E\nup_E dw_E up_A\nup_Z dw_Z up_A up_B" ) - -}); - -QUnit.test( "instrumental variables", function( assert ) { - assert.equal((function(){ - var g = $p( "digraph G { u1 [latent] \n u2 [latent] \n"+ - "u2 -> d -> a -> z \n a -> c -> b -> z \n "+ - "u2 -> y \n b -> u1 -> y \n "+ - "\n }" ) - return _.pluck(GraphAnalyzer.closeSeparator( - g, g.getVertex("y"), g.getVertex("z") - ),"id").join(",") - })(), "b,d" ) - - assert.equal((function(){ - var g = $p( "digraph G { u [latent] \n x [exposure] \n y [outcome] \n"+ - " w -> z -> x -> y \n w -> u -> x \n u -> y \n }" ) - return iv_2_str( GraphAnalyzer.conditionalInstruments( g ) ) - })(), "z | w" ) - - assert.equal((function(){ - var g = $p( "digraph G { u [latent] \n x [exposure] \n y [outcome] \n"+ - " z -> x -> y \n u -> x \n u -> y \n }" ) - return iv_2_str( GraphAnalyzer.conditionalInstruments( g ) ) - })(), "z" ) - - assert.equal((function(){ - var g = $p( "digraph G { u \n w -> z -> x -> y \n w -> u -> x \n u -> y }" ) - return _(GraphAnalyzer.ancestralInstrument( g, g.getVertex("x"), g.getVertex("y"), - g.getVertex("z") )).pluck("id").join(",") - })(), "u" ) - - assert.equal((function(){ - var g = $p( "dag{ u \n z -> x -> y \n u -> x \n u -> y \n }" ) - return ""+_(GraphAnalyzer.conditionalInstruments( g, - g.getVertex("x"), g.getVertex("y"))[0][1]).pluck("id").join(",") - })(), "" ) - - assert.equal((function(){ - var g = $p( "digraph G { u [latent] \n w -> z -> x -> y \n w -> u -> x \n u -> y }" ) - return _(GraphAnalyzer.ancestralInstrument( g, g.getVertex("x"), g.getVertex("y"), - g.getVertex("z") )).pluck("id").join(",") - })(), "w" ) - - assert.equal((function(){ - var g = $p( "digraph G { u [latent] \n z -> x -> y \n u -> x \n u -> y \n }" ) - return ""+_(GraphAnalyzer.ancestralInstrument( g, g.getVertex("x"), g.getVertex("y"), - g.getVertex("z") )).pluck("id").join(",") - })(), "" ) -}); - -QUnit.test( "treeID", function( assert ) { - //instrument - var r = GraphAnalyzer.treeID($p( "dag { Z -> X \n X -> Y \n X <-> Y }" )).results - assert.equal(r["X"][0].instrument, "Z") - assert.equal(r["X"][0].fastp.length, 1) - assert.equal(r["Y"][0].instrument, "Z") - assert.equal(r["Y"][0].fastp.length, 1) - - //instrument + propagate - var r = GraphAnalyzer.treeID($p( "dag { A -> E \n A <-> D \n E -> D \n E -> v1 }" )).results - assert.equal(r["E"][0].instrument, "A") - assert.equal(r["E"][0].fastp.length, 1) - assert.equal(r["v1"][0].instrument, "A") - assert.equal(r["v1"][0].fastp.length, 1) - assert.equal(r["D"][0].propagate, "E") - assert.equal(r["D"][0].fastp.length, 1) - - //missing cycles (example from weihs/drton tsID) - var r = GraphAnalyzer.treeID($p( "dag { 0 -> 1 \n 0 -> 2 \n 0 -> 3 \n 0 <-> 1 \n 0 <-> 2 \n 0 <-> 3 \n 0 <-> 4 \n 3 -> 4 }" )).results - assert.equal(r["1"][0].propagate, "2") - assert.equal(r["1"][0].fastp.length, 1) - assert.equal(r["2"][0].propagate, "4") - assert.equal(r["2"][0].fastp.length, 1) - assert.equal(r["4"][0].propagate, "3") - assert.equal(r["4"][0].fastp.length, 1) - assert.equal("missingCycles" in r["3"][0], true) - assert.equal(r["3"][0].fastp.length, 1) - - //missing cycles (example (4680, 403) from weihs/drton tsID) - var r = GraphAnalyzer.treeID($p( "dag { 3->4 \n 1->2 \n 0->1 \n 2->3 \n 3<->1 \n 3<->0 \n 1<->0 \n 0<->2 \n 0<->4 }" )).results - assert.equal(r["3"][0].propagate, "4") - assert.equal(r["3"][0].fastp.length, 1) - assert.equal(r["4"][0].propagate, "1") - assert.equal(r["4"][0].fastp.length, 1) - assert.equal(r["1"][0].propagate, "2") - assert.equal(r["1"][0].fastp.length, 1) - assert.equal("missingCycles" in r["2"][0], true) - assert.equal(r["2"][0].fastp.length, 1) - - //not identifiable - var r = GraphAnalyzer.treeID($p( "dag { A -> E \n A <-> D \n A <-> E \n A <-> v1 \n D <-> v1 \n E -> D \n E -> v1 }" )).results - assert.equal( ("A" in r) || ("E" in r) || ("D" in r) || ("v1" in r), false ) - - //2-id, missing cycle [[1, 2], [2, 3], [3, 4], [1, 4]] - var r = GraphAnalyzer.treeID($p( "dag { 0 -> 1 \n 0 <-> 1 \n 0 <-> 2 \n 0 <-> 3 \n 0 <-> 4 \n 1 -> 2 \n 1 <-> 3 \n 2 -> 3 \n 2 <-> 4 \n 3 -> 4 }" )).results - assert.equal(r["4"][0].propagate, "3") - assert.equal(r["4"][0].fastp.length, 2) - assert.equal(r["3"][0].propagate, "2") - assert.equal(r["3"][0].fastp.length, 2) - assert.equal(r["2"][0].propagate, "1") - assert.equal(r["2"][0].fastp.length, 2) - assert.equal("missingCycles" in r["1"][0], true) - assert.equal(r["1"][0].fastp.length, 2) - - -}) - -QUnit.test( "graph validation", function( assert ) { - GraphParser.VALIDATE_GRAPH_STRUCTURE = false; - assert.equal( GraphAnalyzer.validate( $p( - "dag { x -> y -> z }" - )), true ) - assert.equal( GraphAnalyzer.validate( $p( - "dag { x -> y -> z -> x }" - )), false ) - - assert.equal( GraphAnalyzer.validate( $p( - "pdag { x -> y -> z }" - )), true ) - - assert.equal( GraphAnalyzer.validate( $p( - "dag { x -- y -> z -> x }" - )), false ) - assert.equal( GraphAnalyzer.validate( $p( - "pdag { x -- y -> z }" - )), true ) - GraphParser.VALIDATE_GRAPH_STRUCTURE = true; -}); - -QUnit.test( "graph types", function( assert ) { - var graphs = { - graph : $p( "graph { x -- y -- z }" ), - dag : $p( "dag { x -> y -> z }" ), - pdag : $p( "pdag { x -- y -> z }" ), - mag : $p( "mag { x <-> y -> z }" ) - }; - - _.each( Object.keys(graphs), function(t){ - assert.equal( GraphTransformer.inducedSubgraph(graphs[t], - graphs[t].getVertex(["x","y"])).getType(), t ) - }); - - _.each( Object.keys(graphs), function(t){ - assert.equal( GraphTransformer.edgeInducedSubgraph(graphs[t], - graphs[t].edges).getType(), t ) - }); - - _.each( Object.keys(graphs), function(t){ - assert.equal( GraphTransformer.ancestorGraph(graphs[t], - graphs[t].getVertex(["x","y"])).getType(), t ) - }); - - _.each( ["backDoorGraph","indirectGraph","activeBiasGraph"], function(f){ - _.each( Object.keys(graphs), function(t){ - assert.equal( GraphTransformer[f](graphs[t], - graphs[t].getVertex(["x"]),graphs[t].getVertex(["z"])).getType(), t ) - }) - }); - - _.each( Object.keys(graphs), function(t){ - assert.equal( GraphTransformer.canonicalDag(graphs[t]).g.getType(), "dag" ) - }); - - _.each( ["moralGraph","skeleton"], function(f){ - _.each( Object.keys(graphs), function(t){ - assert.equal( GraphTransformer[f](graphs[t]).getType(), "graph" ) - }); - }); - -}); - -QUnit.test( "dseparation", function( assert ) { - assert.equal((function(){ - var g = $p( "digraph G { x <-> m -> y }" ) - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [] ) - })(), true ) - - assert.equal((function(){ - var g = $p( "digraph G { x <-> m -> y }" ) - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [g.getVertex("m")] ) - })(), false ) - - assert.equal((function(){ - var g = $p( "digraph G { x <-> m <-> b <-> y }" ) - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [g.getVertex("m")] ) - })(), false ) - - assert.equal((function(){ - var g = $p( "digraph G { x <-> m <-> b <-> y }" ) - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [g.getVertex("m"),g.getVertex("b")] ) - })(), true ) - - assert.equal((function(){ - var g = $p( "digraph G { x -> m -> y }" ) - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [] ) - })(), true ) - - assert.equal((function(){ - var g = $p( "digraph G { x -> m -> y }" ) - return !GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [g.getVertex("m")] ) - })(), true ) - - assert.equal((function(){ - var g = $p( "digraph G { x -> m -> y }" ) - return !GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], [] ) - })(), false ) - - assert.equal((function(){ - var g = $p("dag{R->S->T<-U}") - g = GraphAnalyzer.listPaths( g, false, 1, [g.getVertex("R")], [g.getVertex("U")] )[0] - return GraphAnalyzer.dConnected( g, [g.getVertex("R")], [g.getVertex("U")], - g.getVertex(["T"]) ) - })(), true ) - - assert.equal((function(){ - var g = $p("dag{ a->x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, [g.getVertex("a")], [g.getVertex("y")], - g.getVertex(["m"]) ) - })(), true ) - - assert.equal((function(){ - var g = $p("dag{ a->x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, g.getVertex(["x"]), [g.getVertex("y")], - g.getVertex(["m"]), g.getVertices(["x"]) ) - })(), true ) - - assert.equal((function(){ - var g = $p("dag{ x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [g.getVertex("m")] ) - })(), true ) - - assert.equal((function(){ - var g = $p("dag{ x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - g.getVertex(["p"]), g.getVertex(["m","p"]) ) - })(), true ) - - assert.equal((function(){ - var g = $p("dag{ x[e] y[o] x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - g.getVertex([]), [] ) - })(), false ) - - assert.equal((function(){ - var g = $p("dag{ x[e] y[o] x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - g.getVertex([]), g.getVertex(["m","p"]) ) - })(), true ) - - assert.equal((function(){ - var g = $p("dag{ x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [g.getVertex("m")] ) - })(), true ) - - assert.equal((function(){ - var g = $p("dag{ x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [g.getVertex("p")] ) - })(), true ) - - assert.equal((function(){ - var g = $p("dag{ x->m<-y m->p }") - return GraphAnalyzer.dConnected( g, [g.getVertex("x")], [g.getVertex("y")], - [] ) - })(), false ) - - assert.equal((function(){ - var g = TestGraphs.findExample("onfound") - return GraphAnalyzer.dConnected( g, [g.getVertex("E")], [g.getVertex("B")], - [g.getVertex("Z")] ) - })(), true ) -}); - -QUnit.test( "uncategorized tests", function( assert ) { - -assert.equal((function(){ - var g = TestGraphs.small3(); - g.addSource("S"); - g.addTarget("T"); - g.addAdjustedNode("p"); - var abg = GraphTransformer.activeBiasGraph(g); - var gbd = GraphTransformer.backDoorGraph(abg); - var gbdan = GraphTransformer.ancestorGraph(gbd); - var gam = GraphTransformer.moralGraph( gbdan ) - // the undirected edges from the active bias graph graph should - // not yield an edge x -- y in the moral graph, hence {p} again - // becomes a valid separator - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( abg ) ); -})(), "{p, x}\n{p, z}" ) - -assert.equal((function(){ - var g = TestGraphs.big1(); - g.addAdjustedNode("y"); - var g_bias = GraphTransformer.activeBiasGraph( g ) - g = GraphTransformer.edgeInducedSubgraph( g, g_bias.edges ) - g = GraphTransformer.moralGraph( g ) - g.deleteVertex(g.getVertex("x")) - return sep_2_str( GraphAnalyzer.listMinimalSeparators( g ) ) -})(), "{a, h}\n{e, h}\n{f, h}\n{h, n}" ) - -assert.equal((function(){ - var g = TestGraphs.small2(); - g = GraphTransformer.activeBiasGraph(g); - return g.toAdjacencyList(); -})(), "a b s\nc b t\ng c s" ) - -assert.equal((function(){ - var g = TestGraphs.findExample("Schipf"); - g.addAdjustedNode("WC"); - g.addAdjustedNode("U"); - g = GraphTransformer.activeBiasGraph(g); - return $es(g); -})(), "A -> PA\nA -> S\nA -> TT\nA -> WC\nPA -> T2DM\nPA -> WC\nS -> T2DM\nS -> TT" ) - -assert.equal((function(){ - var g = TestGraphs.extended_confounding_triangle() - var gbias = GraphTransformer.activeBiasGraph(g) - var gmor = GraphTransformer.moralGraph( gbias ) - var gsep = GraphAnalyzer.listMinimalSeparators( - gmor, [g.getVertex("D")], [] ) - return sep_2_str( gsep ) -})(), "{C, D}" ) - -assert.equal((function(){ - var g = TestGraphs.findExample("Acid"); - return sep_2_str( - GraphAnalyzer.listMinimalSeparators( - GraphTransformer.moralGraph( GraphTransformer.activeBiasGraph(g) ), [], g.descendantsOf(g.getSources()) ) ); -})(), "{x1}\n{x4}" ) - -assert.equal((function(){ - var g = TestGraphs.intermediate_adjustment_graph(); - return sep_2_str( - GraphAnalyzer.listMinimalSeparators( - GraphTransformer.moralGraph(GraphTransformer.activeBiasGraph(g)), [], [g.getVertex('I')] ) ); -})(), "{Z}" ) - -assert.equal((function(){ - var g = TestGraphs.intermediate_adjustment_graph(); - g.getVertex('I').latent = true; - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "{Z}" ) - -assert.equal((function(){ - var g = TestGraphs.small_mixed(); - var cc = GraphAnalyzer.connectedComponents(g); - var r = ""; - _.each(cc, function(c) { - r += ("["+_.pluck(c,'id').sort().join(",")+"] "); - } ); - return r; -})(), "[a,b,c] [d,e,f] " ) - -assert.equal((function(){ - // This test verifies that the below methods have - // no side effects (which they had in an earlier, buggy - // version of the code) - var g = TestGraphs.extended_confounding_triangle(); - GraphTransformer.ancestorGraph(g); - GraphTransformer.ancestorGraph(g); - GraphTransformer.activeBiasGraph(g); - GraphTransformer.activeBiasGraph(g); - return g.oldToString(); -})(), "A E\nB O\nC 1\nD 1\nE 1\n\nA B\nC A B\nD A C\nE B C" ) - -assert.equal((function(){ - var g = GraphTransformer.moralGraph( $p( "x 1\ny 1\nm 1\na 1\nb 1\n\nm x\nm y\na m x\nb m y" ) ) - return _.pluck(GraphAnalyzer.connectedComponentAvoiding( g, - [g.getVertex("x")], [g.getVertex("m"), g.getVertex("b")] ),'id') - .sort().join(","); -})(), "a,x" ) - -assert.equal((function(){ - GraphParser.VALIDATE_GRAPH_STRUCTURE = false; - var g = $p( "xobs 1 @0.350,0.000\n"+ -"y 1 @0.562,0.000\n"+ -"t 1 @0.351,-0.017\n"+ -"\n"+ -"xobs y\n"+ -"y t\n"+ -"t xobs" ); - GraphParser.VALIDATE_GRAPH_STRUCTURE = true; - GraphAnalyzer.containsCycle( g ); - return GraphAnalyzer.containsCycle( g ); -})(), "xobs→y→t→xobs" ) - -assert.equal((function(){ - var g = $p( "xobs E @0.350,0.000\n"+ -"y O @0.562,0.000\n"+ -"t 1 @0.351,-0.017\n"+ -"u1 1 @0.476,-0.013\n"+ -"u2 1 @0.175,-0.017\n"+ -"\n"+ -"t xobs\n"+ -"u1 t y\n"+ -"u2 t xobs\n"+ -"xobs y" ); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "{t, u2}\n{u1}" ) - -assert.equal((function(){ - var g = TestGraphs.small5() - g.addAdjustedNode("A") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "T A\nU A I" ) - -assert.equal((function(){ - var g = TestGraphs.small5(); - return GraphTransformer.causalFlowGraph(g).toAdjacencyList(); -})(), "A I\nT A" ) - -assert.equal((function(){ - var g = TestGraphs.small5(); - g.addAdjustedNode("A"); - return GraphTransformer.causalFlowGraph(g).toAdjacencyList(); -})(), "" ) - -assert.equal((function(){ - var g = TestGraphs.small4(); - g.addAdjustedNode("A"); - var abg = GraphTransformer.activeBiasGraph(g); - return abg.oldToString() -})(), "I O\nT E\n\n" ) - -assert.equal((function(){; - var g = TestGraphs.small3(); - g.addAdjustedNode("p"); - g.addSource("S"); - g.addTarget("T"); - var abg = GraphTransformer.activeBiasGraph(g); - g.deleteVertex( "p" ) - return GraphTransformer.edgeInducedSubgraph(g,abg.edges).toAdjacencyList() -})(), "x S\nz T" ) - -assert.equal((function(){; - var g = TestGraphs.small3(); - g.addSource("S"); - g.addTarget("T"); - g.addAdjustedNode("p"); - // this should yield the same result as listing the MSAS of g - // the vertex p should not be listed as contained in the separators - // because it is not listed as compulsory in the call to "listSeparators()" - // (see the api of the function there) - var g_bias = GraphTransformer.activeBiasGraph( g ) - var g_can = GraphTransformer.canonicalDag( g_bias ) - g = GraphTransformer.moralGraph( g_can.g ) - return sep_2_str( GraphAnalyzer.listMinimalSeparators( g, [], - g.getAdjustedNodes() ) ) -})(), "{x}\n{z}" ) - -assert.equal((function(){ - var g = TestGraphs.commentator1(); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "" ) - -assert.equal((function(){ - var g = $p("S E @0.395,0.046\n"+ -"T O @0.715,0.039\n"+ -"h 1 @0.544,0.017\n"+ -"i 1 @0.544,0.017\n"+ -"x 1 @0.424,0.072\n"+ -"z 1 @0.610,0.071\n"+ -"\n"+ -"S T\n"+ -"h S\n"+ -"i h T\n"+ -"x S z\n"+ -"z T\n"); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "{h, x}\n{h, z}\n{i, x}\n{i, z}" ) - -assert.equal((function(){ - var g = TestGraphs.big1(); - g.addAdjustedNode("y"); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "{a, h, x, y}\n{a, h, y, z}\n{e, h, x, y}\n{e, h, y, z}\n{f, h, x, y}\n{f, h, y, z}\n{h, n, x, y}\n{h, n, y, z}" ) - -assert.equal((function(){ - var g = TestGraphs.findExample("Shrier"); - var must = [g.getVertex("FitnessLevel")]; - var must_not = [g.getVertex("Genetics"), - g.getVertex("ConnectiveTissueDisorder"), - g.getVertex("IntraGameProprioception")]; - - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g, must, must_not ) ); -})(), "{Coach, FitnessLevel}\n{FitnessLevel, NeuromuscularFatigue, TissueWeakness}\n{FitnessLevel, TeamMotivation}" ) - -assert.equal((function(){ - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( TestGraphs.very_large_dag() ) ); -})(), "{Allergenexposition, Antibiotika, Begleiterkrankungen, BetreuungKind, Darmflora, Erregerexposition, Geschwister, Hausstaub, Haustiere, Infektionen, RauchenAnderer, RauchenMutter, Stillen}\n{Allergenexposition, Antibiotika, Begleiterkrankungen, Darmflora, Erregerexposition, Geburtsmodus, Hausstaub, Haustiere, Infektionen, RauchenAnderer, RauchenMutter, Stillen}\n{Allergenexposition, Begleiterkrankungen, Darmflora, Erregerexposition, Impfungen, Infektionen, RauchenAnderer, RauchenMutter}" ) - -assert.equal((function(){ - var g = GraphTransformer.moralGraph( $p( "x E\ny O\nm\na\nb\n\nm x\nm y\na m x\nb m y" ) ) - return sep_2_str( GraphAnalyzer.listMinimalSeparators(g) ); -})(), "{a, m}\n{b, m}" ) - -assert.equal((function(){ - // the function "neighboursOf" should, also when called on a vertex set, - // not return any vertices from those sets as neighbours of the set itself - var g = GraphTransformer.moralGraph( $p( "x\ny\nm\na\nb\n\nm x\nm y\na m x\nb m y" ) ) - return _.pluck(g.neighboursOf( [g.getVertex("m"), g.getVertex("b")] ),'id') - .sort().join(","); -})(), "a,x,y" ) - -assert.equal((function(){ - var g = $p("E E @-1.897,0.342\n"+ - "D O @-0.067,0.302\n"+ - "g A @-0.889,1.191\n"+ - "\n"+ - "D g\n"+ - "E D"); - return GraphAnalyzer.violatesAdjustmentCriterion( g ) -})(), true ) - -assert.equal((function(){ - var g = $p("x E @0.083,-0.044\n"+ - "y O @0.571,-0.043\n"+ - "i1 1 @0.331,-0.037\n"+ - "i2 1 @0.328,-0.030\n"+ - "y2 A @0.333,-0.054\n"+ - "y3 1 @0.334,-0.047\n"+ - "\n"+ - "i1 y\n"+ - "i2 y\n"+ - "x y2 i2 i1 y\n"+ - "y3 y x"); - return _.pluck(GraphAnalyzer.intermediates(g),'id').join(","); -})(), "i1,i2" ) - -assert.equal((function(){ - var g = $p("x 1 @0.264,-0.027\n"+ - "y 1 @0.537,-0.015\n"+ - "y2 A @0.216,-0.015\n"+ - "\n"+ - "x y2 y"); - return _.pluck(GraphAnalyzer.intermediates(g),'id').join(","); -})(), "" ) - -assert.equal((function(){ - var g = $p("x E @0.264,-0.027\n"+ -"y O @0.537,-0.015\n"+ -"y2 A @0.216,-0.015\n"+ -"\n"+ -"x y2 y"); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "{y2}" ) - -assert.equal((function(){ - var g = TestGraphs.findExample( "Many variables" ); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "{7}\n{8}" ) - -assert.equal((function(){ - var g = TestGraphs.findExample( "Extended confounding" ); - g.addLatentNode( "A" ); - return imp_2_str( GraphAnalyzer.listMinimalImplications( g ) ); -})(), "" ) - -assert.equal((function(){ - var g = TestGraphs.findExample( "Sebastiani" ); - return imp_2_str( GraphAnalyzer.listMinimalImplications( g, 7 ) ); -})(), "EDN1.3 _||_ SELP.22 | SELP.17, Stroke\n"+ - "EDN1.3 _||_ SELP.22 | ECE1.13, Stroke\n"+ - "EDN1.3 _||_ SELP.22 | ECE1.12, Stroke\n"+ - "EDN1.3 _||_ SELP.22 | ANXA2.8\n"+ - "EDN1.3 _||_ SELP.17 | ECE1.13\n"+ - "EDN1.3 _||_ SELP.17 | ECE1.12, Stroke\n"+ - "EDN1.3 _||_ SELP.17 | ANXA2.8" ) - -assert.equal((function(){ - // X -> I -> Y, I <- M -> Y, I -> A = bias - var g = $p("X E\nY O\nI 1\nJ A\nM 1\n\nX I\nI J Y\nM I Y") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "I J Y\nM I Y\nX I" ) - -assert.equal((function(){ - // X -> I -> Y, I <- M -> Y = bias - var g = $p("X E\nY O\nI A\nM 1\n\nX I\nI Y\nM I Y") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "M I Y\nX I" ) - -assert.equal((function(){ - // X -> I -> Y, I -> A = bias - var g = $p("X E\nY O\nI 1\nJ A\n\nX I\nI Y J") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "I J Y\nX I" ) - -assert.equal((function(){ - // A <- X -> Y = no bias - var g = $p("X E\nY O\nI A\n\nX Y\nX I") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - // X -> Y -> A = bias - var g = $p("X E\nY O\nI A\n\nX Y\nY I") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "X Y\nY I" ) - -assert.equal((function(){ - // X -> A -> Y = no bias - var g = $p("X E\nY O\nI A\n\nX I\nI Y") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - return GraphTransformer.activeBiasGraph(TestGraphs.felixadjust).toAdjacencyList() -})(), "s1 s2 z\ns2 s3\ns3 y\nx s1" ) - -assert.equal((function(){ - var g = $p("A E @1,1\nB O @3,1\nC 1 @2,1\n\nA B\nB C") - return g.hasCompleteLayout() -})(), true ) - -assert.equal((function(){ - var g = $p("A E @1,1\nB O\nC 1 @1,1\n\nA B\nB C") - return g.hasCompleteLayout() -})(), false ) - -assert.equal((function(){ - var g = $p("A E\nB O\nC 1\n\nA B\nB C"); - return g.hasCompleteLayout() -})(), false ) - -assert.equal((function(){ - var g = $p("A E\nB O\nC 1\n\nA B\nB C"); - g.deleteVertex(g.getVertex("A")); - return g.getSources().length; -})(), 0 ) - -assert.equal((function(){ - var g = $p("D O\nD2 O\nE E\nE2 E\n\nD E\nD2 E2") - //console.log(g.toString()) - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "D E\nD2 E2" ) - -assert.equal((function(){ - var g = $p("ein A 1\n"+ -"ein B 1\n"+ -"\n"+ -"ein A ein B"); - return g.oldToString(); -})(), "ein%20A 1\nein%20B 1\n\nein%20A ein%20B" ) - -assert.equal((function(){ - var g = $p("A E\nB O\nE E\nZ\nU\n\nA U\nB Z\nZ E\nU Z"); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "{U, Z}" ) - -assert.equal((function(){ - var g = $p("E E\nD O\nA 1\nB U\nZ 1\n\nA E Z\nB D Z\nZ E D\nE D"); - return GraphAnalyzer.directEffectEqualsTotalEffect( g ) -})(), true ) - -assert.equal((function(){ - var g = $p("E E\nD O\nA 1\nB U\nZ 1\n\nA E Z\nB D Z\nZ D\nE D Z"); - return GraphAnalyzer.directEffectEqualsTotalEffect( g ) -})(), false ) - -assert.equal((function(){ - var g = $p("E E\nD O\nA 1\nB U\nZ 1\n\nA E Z\nB D Z\nZ E D"); - return sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ); -})(), "{A, Z}" ) - -assert.equal((function(){ - var g = $p("E E\nD O\nA 1\nB U\nZ 1\n\nA E Z\nB D Z\nZ E D"); - return sep_2_str( GraphAnalyzer.listMinimalSeparators(GraphTransformer.moralGraph(g)) ); -})(), "{A, Z}\n{B, Z}" ) - -assert.equal((function(){; - var g = TestGraphs.small3(); - g.addAdjustedNode("p"); - var abg = GraphTransformer.activeBiasGraph(g); - return GraphTransformer.edgeInducedSubgraph(g,abg.edges).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - var g = $p("E E @-1.897,0.342\n"+ -"D O @-0.067,0.302\n"+ -"g A @-0.889,1.191\n"+ -"\n"+ -"D g\n"+ -"E D"); - return _.pluck(GraphAnalyzer.nodesThatViolateAdjustmentCriterion( g ),'id').join(","); -})(), "g" ) - -assert.equal((function(){ - var g = $p("X1 E\nX2 E\nY1 O\nY2 O\nU1 1\nU2 1\n\nU1 X1 X2\nU2 Y1 Y2") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - var g = $p("A E\nB O\nC O\nD A\n\nA B\nB C\nC D") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "A B\nB C\nC D" ) - -assert.equal((function(){ - var g = $p("A E\nB O\nC O\n\nA B\nB C") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY1 Y2\nY2 D") - return GraphAnalyzer.directEffectEqualsTotalEffect( g ) -})(), true ) - -assert.equal((function(){ - var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY2 D") - return GraphAnalyzer.directEffectEqualsTotalEffect( g ) -})(), true ) - -assert.equal((function(){ - var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY1 Y2\nY2 D") - return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) -})(), "" ) - -assert.equal((function(){ - var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY2 D") - return sep_2_str( GraphAnalyzer.listMsasDirectEffect( g ) ) -})(), "{D}" ) - -assert.equal((function(){ - var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY1 Y2\nY2 D") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "X1 Y1\nY1 Y2\nY2 D" ) - -assert.equal((function(){ - var g = $p("X1 E\nY1 O\nY2 O\nD A\n\nX1 Y1\nY2 D") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - var g = $p("X1 E\nX2 E\nY1 O\nY2 O\n\nX1 Y1 X2\nY2 X2") - //console.log(g.toString()) - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "Y2 X2" ) - -assert.equal((function(){ - var g = $p("X1 E\nX2 E\n\nX1 X2") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - var g = $p("X1 E\nX2 E\nY1 O\nY2 O\n\nX1 Y1 X2\nX2 Y2") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - var g = $p("X1 E\nX2 E\nY1 O\nY2 O\n\nX1 Y1 X2\nX2 Y2") - return _.pluck( - _.difference(GraphAnalyzer.properPossibleCausalPaths(g),g.getSources()), - "id").sort().join(",") -})(), "Y1,Y2" ) - - -assert.equal((function(){ - // X -> I -> Y, I -> A, adjust I = no bias - var g = $p("X E\nY O\nI A\nJ A\n\nX I\nI J Y") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - // X -> I -> Y, I <- M -> Y, I -> A, adjust M, I = no bias - var g = $p("X E\nY O\nI A\nJ A\nM A\n\nX I\nI J Y\nM I Y") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - // X -> I -> Y, I <- M -> Y, I -> A, adjust M = bias - var g = $p("X E\nY O\nI 1\nJ A\nM A\n\nX I\nI J Y\nM I Y") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "I J Y\nX I" ) - - -assert.equal((function(){ - var g = TestGraphs.findExample( "Shrier" ) - var r = "" - var v1,v2,vv=g.getVertices() - for( var i = 0 ; i < vv.length ; i ++ ){ - for( var j = 0 ; j < vv.length ; j ++ ){ - var c = GraphAnalyzer.minVertexCut( g, [vv[i]], [vv[j]] ) - if( c > 0 ) r += vv[i].id + " " + vv[j].id + " " + c + "\n" - } - } - return r -})(), "WarmUpExercises Injury 1\nCoach WarmUpExercises 2\nCoach Injury 2\nCoach PreGameProprioception 1\nCoach PreviousInjury 1\nCoach IntraGameProprioception 2\nCoach NeuromuscularFatigue 1\nGenetics WarmUpExercises 1\nGenetics Injury 3\nGenetics PreGameProprioception 1\nGenetics TissueWeakness 1\nGenetics IntraGameProprioception 2\nTeamMotivation Injury 1\nTeamMotivation IntraGameProprioception 1\nPreGameProprioception Injury 1\nPreGameProprioception IntraGameProprioception 1\nConnectiveTissueDisorder Injury 2\nConnectiveTissueDisorder IntraGameProprioception 1\nContactSport Injury 1\nFitnessLevel WarmUpExercises 1\nFitnessLevel Injury 2\nFitnessLevel IntraGameProprioception 2\n" ) - -assert.equal((function(){ - var g = $p("X E\nY O\nM 1\nN A\n\nX M\nM N Y") - return GraphTransformer.flowNetwork(g).graph.toAdjacencyList() -})(), "M N X Y\nN M\nX M __SRC\nY M __SNK\n__SNK Y\n__SRC X" ) - -assert.equal((function(){ - var g = $p("X E\nY O\nM 1\nN A\n\nX M\nM N Y") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "M N Y\nX M" ) - -assert.equal((function(){ - var g = $p("X E\nY O\nM 1\nN A\n\nX M\nM N\nY M") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "X M\nY M" ) - -assert.equal((function(){ - // our non-X-ancestor MSAS example from the UAI paper w/o causal edge - var g = $p("X1 E\nX2 E\nY O\nM1 1\nM2 A\n\nX1 M1\nY M2\nM1 M2\nM2 X2") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "M1 M2\nX1 M1\nY M2" ) - -assert.equal((function(){ - var g = $p("X E\nY O\nM 1\n\nY M\nM X") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "M X\nY M" ) - -assert.equal((function(){ - var g = $p("X E\nY O\n\nY X") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "Y X" ) - -assert.equal((function(){ - // our non-X-ancestor MSAS example from the UAI paper w/o causal edge - var g = $p("X1 E\nX2 E\nY O\nM1 1\nM2 1\n\nX1 M1\nY M2\nM1 M2\nM2 X2") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "M2 X2\nY M2" ) - -assert.equal((function(){ - var g = $p("X1 E\nY1 O\nY2 O\nM 1\n\nY1 M\nM Y2") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - -assert.equal((function(){ - var g = $p("X1 E\nX2 E\nY1 O\nY2 O\nU1 1\nU2 1\n\nX1 U1\nU1 X2\nY1 U2\nU2 Y2") - return GraphTransformer.activeBiasGraph(g).toAdjacencyList() -})(), "" ) - - -assert.equal((function(){ - var g = TestGraphs.findExample( "Thoemmes" ) - return GraphAnalyzer.minVertexCut( g, - [g.getVertex("e0"),g.getVertex("s2")], - [g.getVertex("y")] ) -})(), 2 ) - -assert.equal((function(){ - var g = $p("a 1\nb 1\nc 1\nd 1\ne 1\nm 1\nn 1\np 1\nu 1\nx E\ny O\n\n" - +"a b\nb u\nc u n\nd e\ne c\nm a\nn p\np y\nu y\nx c d m") - return GraphAnalyzer.minVertexCut( g, [g.getVertex("x")], [g.getVertex("y")] ) -})(), 2 ) - -assert.equal((function(){ - var g = TestGraphs.findExample( "Confounding" ) - var r = "" - var v1,v2,vv=g.getVertices() - for( var i = 0 ; i < vv.length ; i ++ ){ - for( var j = 0 ; j < vv.length ; j ++ ){ - var c = GraphAnalyzer.minVertexCut( g, [vv[i]], [vv[j]] ) - if( c > 0 ) r += vv[i].id + " " + vv[j].id + " " + c + "\n" - } - } - return r -})(), "A D 2\nB E 1\n" ) - -assert.equal((function(){ - var g = $p( - "digraph G { y -> m \n m -> x }" - ).addSource("x").addTarget("y") - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "m -> x\ny -> m" ) - -assert.equal((function(){ - var g = $p( - "digraph G { x [exposure]\ny [outcome]\na [adjusted]\n"+ - "x -> a\nu -> x\nu -> y }" ) - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "u -> x\nu -> y" ) - -assert.equal((function(){ - var g = $p( - "digraph G { x [exposure]\ny [outcome]\na [adjusted]\n b [adjusted]\n"+ - "x -> a\nx -> b\nb -> a\ny -> b }" ) - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "x -> b\ny -> b" ) - -assert.equal((function(){ - var g = $p( - "digraph G { x [exposure]\ny [outcome]\nm [adjusted]\nx -> m\ny -> m }" ) - g = GraphTransformer.canonicalDag( g ).g - g = GraphTransformer.ancestorGraph( g ) - return ""+$es(g) -})(), "x -> m\ny -> m" ) - -assert.equal((function(){ - var g = $p( - "digraph G { x [exposure]\ny [outcome]\nm [adjusted]\nx -> m\ny -> m }" ) - return $es(GraphTransformer.ancestorGraph(g)) -})(), "x -> m\ny -> m" ) - -assert.equal((function(){ - var g = $p( - "digraph G { b [exposure]\nc [outcome]\na [adjusted]\na -- b\na -- c }" ) - return GraphAnalyzer.connectedComponents(g).length -})(), 1 ) - -assert.equal((function(){ - var g = $p( - "digraph G {a -- b }" ) - return GraphAnalyzer.connectedComponents(g).length -})(), 1 ) - -assert.equal((function(){ - var g = $p( - "digraph G {a [exposure]\nb [outcome]\nc [adjusted]\na -> c\nb -> c }" ) - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "a -> c\nb -> c" ) - -assert.equal((function(){ - var g = $p( - "digraph G {a [exposure]\nb [outcome]\nc [adjusted]\na -> b\nb -> c }" ) - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "a -> b\nb -> c" ) - -assert.equal((function(){ - var g = TestGraphs.findExample("Acid") - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "x1 -> x3\nx1 -> x4\nx10 -> x15\nx4 -> x5\nx5 -> x7\nx7 -> x9\nx9 -> x10" ) - -assert.equal((function(){ - var g = $p( - "digraph G { a [exposure] \n b [outcome] \n a <-> c \n c -> b }" ) - return $es(GraphTransformer.canonicalDag(g).g) -})(), "L1 -> a\nL1 -> c\nc -> b" ) - -assert.equal((function(){ - var g = $p( - "digraph G { a [exposure] \n b [outcome] \n a <-> c \n c -> b }" ) - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "a <-> c\nc -> b" ) - -assert.equal((function(){ - var g = $p( "digraph G { a -- b }" ) - var ids = _.pluck(GraphAnalyzer.connectedComponentAvoiding( g, [g.getVertex("a")] ) - ,"id") - ids=ids.sort(); return ids.join(" ") -})(), "a b" ) - -assert.equal((function(){ - var g = $p( - "digraph G { x [source] \n y [outcome] \n a [adjusted] \n " - +"a -> x \n b -> a \n b -> y }" - ) - return $es(GraphTransformer.activeBiasGraph(g)) -})(), "" ) - - -assert.equal( _.pluck(GraphAnalyzer.dpcp( - $p( "digraph{ x [exposure]\n y [outcome]\n x -> y -- z }" ) ),"id"). - sort().join(","), - "y,z" ) - -assert.equal( _.pluck(GraphAnalyzer.dpcp( - $p( "digraph{ x [exposure]\n y [outcome]\n x -- y -- z }" ) ),"id"). - sort().join(","), - "x,y,z" ) - - -}); // end uncategorized tests From 9a1f54c3614a0a5435ed7771e2f90ba5024a13c7 Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Thu, 3 Mar 2022 15:03:48 -0800 Subject: [PATCH 10/17] deleting legacy test HTML --- test/.gitignore | 1 + test/test-helpers.js | 38 -------------------------------------- test/tests.html | 19 ------------------- test/tests.js | 11 ----------- 4 files changed, 1 insertion(+), 68 deletions(-) create mode 100644 test/.gitignore delete mode 100644 test/test-helpers.js delete mode 100644 test/tests.html delete mode 100644 test/tests.js diff --git a/test/.gitignore b/test/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/test/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/test/test-helpers.js b/test/test-helpers.js deleted file mode 100644 index c58dd22..0000000 --- a/test/test-helpers.js +++ /dev/null @@ -1,38 +0,0 @@ - -function sep_2_str( ss ){ - var r = []; - if( ss.length == 0 ) - return ""; - _.each( ss, function(s){ - var rs = _.pluck( s, 'id').sort().join(", "); - r.push(rs); - }); - r.sort(); - return "{"+r.join("}\n{")+"}"; -} - -function imp_2_str( imp ){ - var r = [],j,rr; - _.each( imp, function( i ){ - for( j = 0 ; j < i[2].length ; j ++ ){ - rr = i[0]+" _||_ "+i[1]; - if( i[2][j].length > 0 ){ - rr += " | "+_.pluck( i[2][j], 'id').sort().join(", "); - } - r.push(rr) - } - } ); - return r.join("\n"); -} - -function iv_2_str( ivs ){ - var r = [] - _(ivs).each( function( i ){ - if( i[1].length > 0 ){ - r.push( i[0].id+" | "+_(i[1]).pluck('id').sort().join(", ") ) - } else { - r.push( i[0].id ) - } - } ) - return r.sort().join("\n") -} diff --git a/test/tests.html b/test/tests.html deleted file mode 100644 index 0fc63fa..0000000 --- a/test/tests.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - QUnit tests - - - -
    -
    - - - - - - - - - \ No newline at end of file diff --git a/test/tests.js b/test/tests.js deleted file mode 100644 index 2bc01f4..0000000 --- a/test/tests.js +++ /dev/null @@ -1,11 +0,0 @@ -GraphParser.VALIDATE_GRAPH_STRUCTURE = true; - -var $p = function(s){ return GraphParser.parseGuess(s) } -var $es = function(g){ return GraphSerializer.toDotEdgeStatements(g) } - - - - - - - From 8f233b7c1cd3f8caac2045c0811b6ca14a9baf45 Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Fri, 4 Mar 2022 11:28:03 -0800 Subject: [PATCH 11/17] fix bug in kins function --- jslib/graph/GraphTransformer.js | 3 +++ r/R/internal.r | 7 +++++-- r/tests/testthat/testMarkovBlanket.R | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 r/tests/testthat/testMarkovBlanket.R diff --git a/jslib/graph/GraphTransformer.js b/jslib/graph/GraphTransformer.js index ede7c17..ea5fcc4 100644 --- a/jslib/graph/GraphTransformer.js +++ b/jslib/graph/GraphTransformer.js @@ -259,6 +259,9 @@ var GraphTransformer = { */ activeSelectionBiasGraph : function( g, x, y, S ){ var r = new Graph() + x = g.getVertex(x) + y = g.getVertex(y) + S = _.map( S, g.getVertex, g ) _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) g = g.clone() _.each( S, function(s){ g.removeSelectedNode( s ) } ) diff --git a/r/R/internal.r b/r/R/internal.r index 2e263d6..234ceca 100644 --- a/r/R/internal.r +++ b/r/R/internal.r @@ -168,19 +168,22 @@ V8::JS( paste0( ... ) ) } + .kins <- function( x, v, type="descendants" ){ supported <- c("descendants","ancestors","neighbours","posteriors","anteriors", "children","parents","spouses","adjacentNodes") if( ! type %in% supported ){ stop("Supported kinship types : ",paste(supported,collapse=", ") ) } - .checkName( x, v ) + if( length(v) == 0 ){ + return( character(0) ) + } + .checkAllNames( x, v ) r <- c() xv <- .getJSVar() vv <- .getJSVar() tryCatch({ .jsassigngraph( xv, x ) - for( w in v ){ .jsassign( vv, as.character(w) ) .jsassign( vv, .jsp("global.",xv,".getVertex(global.",vv,")") ) diff --git a/r/tests/testthat/testMarkovBlanket.R b/r/tests/testthat/testMarkovBlanket.R new file mode 100644 index 0000000..bb028a2 --- /dev/null +++ b/r/tests/testthat/testMarkovBlanket.R @@ -0,0 +1,18 @@ + +fig_2.9 <- dagitty( + 'dag{ + W -> Y + X -> W + Z_1 -> {X Z_3} + Z_2 -> {Y Z_3} + Z_3 -> {X Y} + }' + ) + +test_that("Markov Blanket", { + expect_equal( sort( markovBlanket(fig_2.9, "Z_1") ) , c("X", "Z_2", "Z_3") ) + expect_equal( sort( markovBlanket(fig_2.9, "Y") ), c("W", "Z_2", "Z_3") ) +} ) + + + From 79b1a78c8ab184a464b32689dea91d2a0b58fdfc Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Fri, 4 Mar 2022 11:36:30 -0800 Subject: [PATCH 12/17] further tests Markov blanket --- r/tests/testthat/testMarkovBlanket.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/r/tests/testthat/testMarkovBlanket.R b/r/tests/testthat/testMarkovBlanket.R index bb028a2..b564c7f 100644 --- a/r/tests/testthat/testMarkovBlanket.R +++ b/r/tests/testthat/testMarkovBlanket.R @@ -10,8 +10,11 @@ fig_2.9 <- dagitty( ) test_that("Markov Blanket", { - expect_equal( sort( markovBlanket(fig_2.9, "Z_1") ) , c("X", "Z_2", "Z_3") ) - expect_equal( sort( markovBlanket(fig_2.9, "Y") ), c("W", "Z_2", "Z_3") ) + expect_equal( sort( markovBlanket( fig_2.9, c() ) ), character(0) ) + expect_equal( sort( markovBlanket( fig_2.9, list() ) ), character(0) ) + expect_equal( sort( markovBlanket( fig_2.9, "Z_1" ) ), c("X", "Z_2", "Z_3") ) + expect_equal( sort( markovBlanket( fig_2.9, "Y" ) ), c("W", "Z_2", "Z_3") ) + expect_equal( sort( markovBlanket( fig_2.9, c("W","Y") ) ), c("X", "Z_2", "Z_3") ) } ) From 4eabad3fc2fd7846c01c65f910301ec949142da7 Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Thu, 10 Mar 2022 20:36:09 -0800 Subject: [PATCH 13/17] adjustment sets with selection nodes --- gui/js/dagitty.js | 91 ++++++++++++++++++++++++++---------- jslib/graph/GraphAnalyzer.js | 88 ++++++++++++++++++++++++---------- r/inst/js/dagitty-alg.js | 91 ++++++++++++++++++++++++++---------- test/test/adjustment-dags.js | 9 ++++ test/test/selection-nodes.js | 54 +++++++++++++++++---- 5 files changed, 250 insertions(+), 83 deletions(-) diff --git a/gui/js/dagitty.js b/gui/js/dagitty.js index 5bce39d..4ff374c 100644 --- a/gui/js/dagitty.js +++ b/gui/js/dagitty.js @@ -1373,56 +1373,94 @@ var GraphAnalyzer = { /** * Determines whether Z is a valid adjustment set in g, with possible selection bias - * due to conditioning on nodes S. + * due to conditioning on nodes S. The nodes S and/or Z can be omitted, in which case they are + * taken as pre-defined from the DAG syntax (using the "adjusted" or "selected" keywords) */ isAdjustmentSet : function( g, Z, S ){ var gtype = g.getType() - var Zg = _.map( Z, g.getVertex, g ) if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute adjustment sets for graph of type "+gtype ) } if( g.getSources().length == 0 || g.getTargets().length == 0 ){ return false } - + if( Z == null ){ + Z = g.getAdjustedNodes() + } + if( S == null ){ + S = g.getSelectedNodes() + } + var Zg = _.map( Z, g.getVertex, g ) if( _.intersection( this.dpcp(g), Zg ).length > 0 ){ return false } var gbd = GraphTransformer.backDoorGraph(g) + var Sgbd = _.map( S, gbd.getVertex, gbd ) var Zgbd = _.map( Zg, gbd.getVertex, gbd ) - var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd ) - if( S && S.length > 0 ){ - var Sg = _.map( S, g.getVertex, g ) - r = r && !this.dConnected( g, Zg, Sg, [] ) - r = r && !this.dConnected( g, g.getTargets(), Sg, g.getSources().concat( Zg ) ) + var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd.concat( Sgbd ) ) + if( S.length > 0 ){ + r = r && !this.dConnected( gbd, gbd.getTargets(), Sgbd ) } return r }, - listMsasTotalEffect : function( g, must, must_not, max_nr ){ + /** + * Lists all minimal sufficient adjustment sets containing nodes in M (mandatory nodes) but not + * F (forbidden nodes). Selection nodes can also be defined within the DAG. + */ + listMsasTotalEffect : function( g, M, F, max_nr ){ var gtype = g.getType() if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute total affect adjustment sets for graph of type "+gtype ) } + + if( !g || g.getSources().length < 1 || g.getTargets().length < 1 ){ return [] } + + var gg = g if( gtype == "pdag" ){ - g = GraphTransformer.cgToRcg( g ) - } - if( !g ){ return [] } + gg = GraphTransformer.cgToRcg( g ) + } + if( M ){ + _.each( M, function(v){ gg.addAdjustedNode( v ) } ) + } + if( F ){ + _.each( F, function(v){ gg.addLatentNode( v ) } ) + } - if(GraphAnalyzer.violatesAdjustmentCriterion(g)){ return [] } - var adjusted_nodes = g.getAdjustedNodes() - var latent_nodes = g.getLatentNodes().concat( this.dpcp(g) ) - - var gam = GraphTransformer.moralGraph( - GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph(g) ) ) - - if( must ) - adjusted_nodes = adjusted_nodes.concat( must ) - if( must_not ) - latent_nodes = latent_nodes.concat( must_not ) + + if( GraphAnalyzer.violatesAdjustmentCriterion(gg)){ return [] } - return this.listMinimalSeparators( gam, adjusted_nodes, latent_nodes, max_nr ) + var gbd = GraphTransformer.backDoorGraph(gg) + var S = gg.getSelectedNodes() + if( S.length > 0 ){ + if( this.dConnected( gbd, gbd.getTargets(), _.map(S, Graph.getVertex, gbd ) ) ){ + return [] + } else { + _.each( S, function(s){ gbd.removeSelectedNode( s ); gbd.addAdjustedNode( s ) } ) + } + } + + var gam = GraphTransformer.moralGraph( GraphTransformer.ancestorGraph( gbd ) ) + + var adjusted_nodes = _.map( gg.getAdjustedNodes(), Graph.getVertex, gam ) + var latent_nodes = _.map( gg.getLatentNodes().concat( this.dpcp(gg) ), Graph.getVertex, gam ) + + // at this point, "latent_nodes" may contain + // undefined values because not all adjusted or latent nodes may have beeen + // retained in the moral graph. Therefore, we use "compact" to remove such + // values. + var r = this.listMinimalSeparators( gam, + adjusted_nodes, + _.compact(latent_nodes), max_nr ) + + // Give back vertex objects from original graph, rather than the constructed + // ancestor moral graph. + S = _.map( S, Graph.getVertex, g ) + for( i = 0 ; i < r.length ; i ++ ){ + r[i] = _.map( r[i], Graph.getVertex, g ) + r[i] = _.difference( r[i], S ) + } + return r }, canonicalAdjustmentSet : function( g ){ @@ -3954,6 +3992,9 @@ var GraphTransformer = { */ activeSelectionBiasGraph : function( g, x, y, S ){ var r = new Graph() + x = g.getVertex(x) + y = g.getVertex(y) + S = _.map( S, g.getVertex, g ) _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) g = g.clone() _.each( S, function(s){ g.removeSelectedNode( s ) } ) diff --git a/jslib/graph/GraphAnalyzer.js b/jslib/graph/GraphAnalyzer.js index 22a9da6..840770b 100644 --- a/jslib/graph/GraphAnalyzer.js +++ b/jslib/graph/GraphAnalyzer.js @@ -383,56 +383,94 @@ var GraphAnalyzer = { /** * Determines whether Z is a valid adjustment set in g, with possible selection bias - * due to conditioning on nodes S. + * due to conditioning on nodes S. The nodes S and/or Z can be omitted, in which case they are + * taken as pre-defined from the DAG syntax (using the "adjusted" or "selected" keywords) */ isAdjustmentSet : function( g, Z, S ){ var gtype = g.getType() - var Zg = _.map( Z, g.getVertex, g ) if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute adjustment sets for graph of type "+gtype ) } if( g.getSources().length == 0 || g.getTargets().length == 0 ){ return false } - + if( Z == null ){ + Z = g.getAdjustedNodes() + } + if( S == null ){ + S = g.getSelectedNodes() + } + var Zg = _.map( Z, g.getVertex, g ) if( _.intersection( this.dpcp(g), Zg ).length > 0 ){ return false } var gbd = GraphTransformer.backDoorGraph(g) + var Sgbd = _.map( S, gbd.getVertex, gbd ) var Zgbd = _.map( Zg, gbd.getVertex, gbd ) - var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd ) - if( S && S.length > 0 ){ - var Sg = _.map( S, g.getVertex, g ) - r = r && !this.dConnected( g, Zg, Sg, [] ) - r = r && !this.dConnected( g, g.getTargets(), Sg, g.getSources().concat( Zg ) ) + var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd.concat( Sgbd ) ) + if( S.length > 0 ){ + r = r && !this.dConnected( gbd, gbd.getTargets(), Sgbd ) } return r }, - listMsasTotalEffect : function( g, must, must_not, max_nr ){ + /** + * Lists all minimal sufficient adjustment sets containing nodes in M (mandatory nodes) but not + * F (forbidden nodes). Selection nodes can also be defined within the DAG. + */ + listMsasTotalEffect : function( g, M, F, max_nr ){ var gtype = g.getType() if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute total affect adjustment sets for graph of type "+gtype ) } + + if( !g || g.getSources().length < 1 || g.getTargets().length < 1 ){ return [] } + + var gg = g if( gtype == "pdag" ){ - g = GraphTransformer.cgToRcg( g ) - } - if( !g ){ return [] } + gg = GraphTransformer.cgToRcg( g ) + } + if( M ){ + _.each( M, function(v){ gg.addAdjustedNode( v ) } ) + } + if( F ){ + _.each( F, function(v){ gg.addLatentNode( v ) } ) + } - if(GraphAnalyzer.violatesAdjustmentCriterion(g)){ return [] } - var adjusted_nodes = g.getAdjustedNodes() - var latent_nodes = g.getLatentNodes().concat( this.dpcp(g) ) - - var gam = GraphTransformer.moralGraph( - GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph(g) ) ) - - if( must ) - adjusted_nodes = adjusted_nodes.concat( must ) - if( must_not ) - latent_nodes = latent_nodes.concat( must_not ) + + if( GraphAnalyzer.violatesAdjustmentCriterion(gg)){ return [] } - return this.listMinimalSeparators( gam, adjusted_nodes, latent_nodes, max_nr ) + var gbd = GraphTransformer.backDoorGraph(gg) + var S = gg.getSelectedNodes() + if( S.length > 0 ){ + if( this.dConnected( gbd, gbd.getTargets(), _.map(S, Graph.getVertex, gbd ) ) ){ + return [] + } else { + _.each( S, function(s){ gbd.removeSelectedNode( s ); gbd.addAdjustedNode( s ) } ) + } + } + + var gam = GraphTransformer.moralGraph( GraphTransformer.ancestorGraph( gbd ) ) + + var adjusted_nodes = _.map( gg.getAdjustedNodes(), Graph.getVertex, gam ) + var latent_nodes = _.map( gg.getLatentNodes().concat( this.dpcp(gg) ), Graph.getVertex, gam ) + + // at this point, "latent_nodes" may contain + // undefined values because not all adjusted or latent nodes may have beeen + // retained in the moral graph. Therefore, we use "compact" to remove such + // values. + var r = this.listMinimalSeparators( gam, + adjusted_nodes, + _.compact(latent_nodes), max_nr ) + + // Give back vertex objects from original graph, rather than the constructed + // ancestor moral graph. + S = _.map( S, Graph.getVertex, g ) + for( i = 0 ; i < r.length ; i ++ ){ + r[i] = _.map( r[i], Graph.getVertex, g ) + r[i] = _.difference( r[i], S ) + } + return r }, canonicalAdjustmentSet : function( g ){ diff --git a/r/inst/js/dagitty-alg.js b/r/inst/js/dagitty-alg.js index b260532..ec487b1 100644 --- a/r/inst/js/dagitty-alg.js +++ b/r/inst/js/dagitty-alg.js @@ -1368,56 +1368,94 @@ var GraphAnalyzer = { /** * Determines whether Z is a valid adjustment set in g, with possible selection bias - * due to conditioning on nodes S. + * due to conditioning on nodes S. The nodes S and/or Z can be omitted, in which case they are + * taken as pre-defined from the DAG syntax (using the "adjusted" or "selected" keywords) */ isAdjustmentSet : function( g, Z, S ){ var gtype = g.getType() - var Zg = _.map( Z, g.getVertex, g ) if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute adjustment sets for graph of type "+gtype ) } if( g.getSources().length == 0 || g.getTargets().length == 0 ){ return false } - + if( Z == null ){ + Z = g.getAdjustedNodes() + } + if( S == null ){ + S = g.getSelectedNodes() + } + var Zg = _.map( Z, g.getVertex, g ) if( _.intersection( this.dpcp(g), Zg ).length > 0 ){ return false } var gbd = GraphTransformer.backDoorGraph(g) + var Sgbd = _.map( S, gbd.getVertex, gbd ) var Zgbd = _.map( Zg, gbd.getVertex, gbd ) - var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd ) - if( S && S.length > 0 ){ - var Sg = _.map( S, g.getVertex, g ) - r = r && !this.dConnected( g, Zg, Sg, [] ) - r = r && !this.dConnected( g, g.getTargets(), Sg, g.getSources().concat( Zg ) ) + var r = !this.dConnected( gbd, gbd.getSources(), gbd.getTargets(), Zgbd.concat( Sgbd ) ) + if( S.length > 0 ){ + r = r && !this.dConnected( gbd, gbd.getTargets(), Sgbd ) } return r }, - listMsasTotalEffect : function( g, must, must_not, max_nr ){ + /** + * Lists all minimal sufficient adjustment sets containing nodes in M (mandatory nodes) but not + * F (forbidden nodes). Selection nodes can also be defined within the DAG. + */ + listMsasTotalEffect : function( g, M, F, max_nr ){ var gtype = g.getType() if( gtype != "dag" && gtype != "pdag" && gtype != "mag" && gtype != "pag" ){ throw( "Cannot compute total affect adjustment sets for graph of type "+gtype ) } + + if( !g || g.getSources().length < 1 || g.getTargets().length < 1 ){ return [] } + + var gg = g if( gtype == "pdag" ){ - g = GraphTransformer.cgToRcg( g ) - } - if( !g ){ return [] } + gg = GraphTransformer.cgToRcg( g ) + } + if( M ){ + _.each( M, function(v){ gg.addAdjustedNode( v ) } ) + } + if( F ){ + _.each( F, function(v){ gg.addLatentNode( v ) } ) + } - if(GraphAnalyzer.violatesAdjustmentCriterion(g)){ return [] } - var adjusted_nodes = g.getAdjustedNodes() - var latent_nodes = g.getLatentNodes().concat( this.dpcp(g) ) - - var gam = GraphTransformer.moralGraph( - GraphTransformer.ancestorGraph( - GraphTransformer.backDoorGraph(g) ) ) - - if( must ) - adjusted_nodes = adjusted_nodes.concat( must ) - if( must_not ) - latent_nodes = latent_nodes.concat( must_not ) + + if( GraphAnalyzer.violatesAdjustmentCriterion(gg)){ return [] } - return this.listMinimalSeparators( gam, adjusted_nodes, latent_nodes, max_nr ) + var gbd = GraphTransformer.backDoorGraph(gg) + var S = gg.getSelectedNodes() + if( S.length > 0 ){ + if( this.dConnected( gbd, gbd.getTargets(), _.map(S, Graph.getVertex, gbd ) ) ){ + return [] + } else { + _.each( S, function(s){ gbd.removeSelectedNode( s ); gbd.addAdjustedNode( s ) } ) + } + } + + var gam = GraphTransformer.moralGraph( GraphTransformer.ancestorGraph( gbd ) ) + + var adjusted_nodes = _.map( gg.getAdjustedNodes(), Graph.getVertex, gam ) + var latent_nodes = _.map( gg.getLatentNodes().concat( this.dpcp(gg) ), Graph.getVertex, gam ) + + // at this point, "latent_nodes" may contain + // undefined values because not all adjusted or latent nodes may have beeen + // retained in the moral graph. Therefore, we use "compact" to remove such + // values. + var r = this.listMinimalSeparators( gam, + adjusted_nodes, + _.compact(latent_nodes), max_nr ) + + // Give back vertex objects from original graph, rather than the constructed + // ancestor moral graph. + S = _.map( S, Graph.getVertex, g ) + for( i = 0 ; i < r.length ; i ++ ){ + r[i] = _.map( r[i], Graph.getVertex, g ) + r[i] = _.difference( r[i], S ) + } + return r }, canonicalAdjustmentSet : function( g ){ @@ -3949,6 +3987,9 @@ var GraphTransformer = { */ activeSelectionBiasGraph : function( g, x, y, S ){ var r = new Graph() + x = g.getVertex(x) + y = g.getVertex(y) + S = _.map( S, g.getVertex, g ) _.each( g.getVertices(), function(v){ r.addVertex(v.id) } ) g = g.clone() _.each( S, function(s){ g.removeSelectedNode( s ) } ) diff --git a/test/test/adjustment-dags.js b/test/test/adjustment-dags.js index b9c80bb..c2f48e5 100644 --- a/test/test/adjustment-dags.js +++ b/test/test/adjustment-dags.js @@ -20,6 +20,15 @@ QUnit.module("dagitty") QUnit.test( "adjustment in DAGs", function( assert ) { + assert.equal( + sep_2_str(GraphAnalyzer.listMsasTotalEffect($p("{x[e]} <- a -> m <- b -> {y[o]}"))), + "{}") + + assert.equal( + sep_2_str(GraphAnalyzer.listMsasTotalEffect($p("{x[e]} <- a -> m <- b -> {y[o]}"),['m'])), + "{a, m}\n{b, m}") + + assert.equal((function(){ var g = $p("pdag{ x -> {a -- b -- c -- a } b -> y x[e] y[o] }") return _.pluck(GraphAnalyzer.properPossibleCausalPaths(g),"id").sort().join(" ") diff --git a/test/test/selection-nodes.js b/test/test/selection-nodes.js index 2278cdc..34782f0 100644 --- a/test/test/selection-nodes.js +++ b/test/test/selection-nodes.js @@ -1,18 +1,56 @@ -const dagitty = require("../../jslib/dagitty-node.js") +const {GraphParser, GraphAnalyzer, Graph} = require("../../jslib/dagitty-node.js") +const sep_2_str = (ss) => { + var r = []; + if( ss.length == 0 ) + return ""; + ss.forEach( (s) => r.push( s.map( x => x.id ).sort().join(", ") ) ) + r.sort(); + return "{"+r.join("}\n{")+"}"; +} + QUnit.module('dagitty') + QUnit.test('adjustment in DAG with selection nodes', assert => { - let g = dagitty.GraphParser.parseGuess( "S1 -> {X[exposure]} -> {Y[outcome]} <- S2" ) + let g = new Graph( "S1 -> {X[exposure]} -> {Y[outcome]} <- S2" ) + + assert.true( GraphAnalyzer.isAdjustmentSet( g, [] ) ) + assert.true( GraphAnalyzer.isAdjustmentSet( g, [], [] ) ) + assert.true( GraphAnalyzer.isAdjustmentSet( g, [], ["S1"] ) ) + + assert.false( GraphAnalyzer.isAdjustmentSet( g, [], ["S2"] ) ) + assert.true( GraphAnalyzer.isAdjustmentSet( g, ["S2"], ["S1"] ) ) + assert.false( GraphAnalyzer.isAdjustmentSet( g, ["S1"], ["S2"] ) ) + + g = new Graph( "{S1 [selected]} -> {X[exposure]} -> {Y[outcome]} <- {S2 [adjusted]}" ) + + assert.true( GraphAnalyzer.isAdjustmentSet( g ) ) + + g = new Graph( "{S1 [s]} -> {X[e]} -> {Y[o]} <- {S2 [a]}" ) - assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, [] ), true ) - assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, [], [] ), true ) - assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, [], ["S1"] ), true ) + assert.true( GraphAnalyzer.isAdjustmentSet( g ) ) + g = new Graph( "{S1 [a]} -> {X[e]} -> {Y[o]} <- {S2 [s]}" ) - assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, [], ["S2"] ), false ) - assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, ["S2"], ["S1"] ), true ) - assert.equal( dagitty.GraphAnalyzer.isAdjustmentSet( g, ["S1"], ["S2"] ), false ) + assert.false( GraphAnalyzer.isAdjustmentSet( g ) ) + g = new Graph( "{S1 [s]} -> {X[e]} <- {Z[a]} -> {Y[o]} <- {S2 [a]}" ) + + assert.false( GraphAnalyzer.isAdjustmentSet( g, [] ) ) + assert.true( GraphAnalyzer.isAdjustmentSet( g ) ) + + g = new Graph( "{S1 [s]} <- {X[e]} <- {Z[a]} -> {Y[o]} <- {S2 [a]}" ) + + assert.false( GraphAnalyzer.isAdjustmentSet( g, [] ) ) + assert.false( GraphAnalyzer.isAdjustmentSet( g ) ) + + g = new Graph( "{S1 [s]} -> {X[e]} <- Z -> {Y[o]} <- S2" ) + assert.equal( sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ), "{Z}" ) + + g = new Graph( "{S1 [s]} <- {X[e]} <- Z -> {Y[o]} <- S2" ) + assert.equal( sep_2_str( GraphAnalyzer.listMsasTotalEffect( g ) ), "" ) }); + + From 383c9bee3f1e0dd99116db9591606104d27aceb7 Mon Sep 17 00:00:00 2001 From: Johannes Textor Date: Wed, 16 Mar 2022 23:09:16 -0700 Subject: [PATCH 14/17] making progress on selection nodes --- gui/dags.css | 10 ++ gui/dags.html | 15 +- gui/js/dagitty.js | 63 ++++++++- gui/js/main.js | 229 +++++++++++++++++++------------ jslib/graph/GraphAnalyzer.js | 48 ++++++- jslib/graph/GraphTransformer.js | 8 +- jslib/gui/GraphGUI_Controller.js | 5 +- jslib/gui/GraphGUI_View.js | 2 +- r/inst/js/dagitty-alg.js | 56 +++++++- test/test/adjustment-dags.js | 17 ++- 10 files changed, 340 insertions(+), 113 deletions(-) diff --git a/gui/dags.css b/gui/dags.css index 8603fd1..912d401 100644 --- a/gui/dags.css +++ b/gui/dags.css @@ -175,6 +175,16 @@ form{ margin: 2em; } +p.warning { + color: #a00; + font-weight: bold; +} +p.assurance { + color: #0a0; + font-weight: bold; +} + + @media screen and (max-width: 40em) { main{ height: 90%; diff --git a/gui/dags.html b/gui/dags.html index d1e0fa9..da44e50 100644 --- a/gui/dags.html +++ b/gui/dags.html @@ -260,20 +260,20 @@

    -

    -

    +

    +

    + onclick="GUI.set_view_mode(this.value)" value="causalodds"/>

    + onclick="GUI.set_view_mode(this.value)" value="moral"/>

    + onclick="GUI.set_view_mode(this.value)" value="dependency"/>

    + onclick="GUI.set_view_mode(this.value)" value="equivalence"/>

    @@ -377,6 +377,7 @@

    ", "OK" - ) -} - function exportTikzCode(){ DAGittyControl.getView().openHTMLDialog( "