---
title: "Comparisons with Other Packages"
output: rmarkdown::html_vignette
bibliography: references.bib
vignette: >
  %\VignetteIndexEntry{Comparisons with Other Packages}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

In this vignette, we compare `caugi` to some of the most widely used graph
packages in R, Python, and Java.

## Overview

The following table summarizes the packages included in this comparison, their
general focus, and the languages they are packaged for.

  | Package     | Type                            | Language                        | URL                                         |
  | :---------- | ------------------------------- | ------------------------------- | :------------------------------------------ |
  | igraph      | General-purpose                 | R, Python, C                    | <https://igraph.org>                        |
  | graph       | General-purpose                 | R                               | <https://github.com/Bioconductor/graph>     |
  | gRbase      | Graphical models                | R                               | <https://CRAN.R-project.org/package=gRbase> |
  | pcalg       | Causal graphs                   | R                               | <https://pcalg.r-forge.r-project.org>       |
  | dagitty     | Causal graphs                   | R, Web                          | <https://www.dagitty.net>                   |
  | bnlearn     | Bayesian networks               | R                               | <https://www.bnlearn.com>                   |
  | ggm         | Graphical Markov models         | R                               | <https://CRAN.R-project.org/package=ggm>    |
  | MixedGraphs | Causal graphs                   | R                               | <https://github.com/rje42/MixedGraphs>      |
  | NetworkX    | General-purpose                 | Python                          | <https://networkx.org>                      |
  | pgmpy       | Probabilitistc graphical models | Python                          | <https://github.com/pgmpy/pgmpy>            |
  | Tetrad      | Causal graphs                   | Java, CLI, R[^tetrad-r], Python | <http://www.phil.cmu.edu/tetrad>            |

  : An overview of the packages included in this comparison

[^tetrad-r]: Bindings for Tetrad exist in
    <https://github.com/cmu-phil/r-tetrad>, but not in the form of a formal R
    package. The authors instead recommend using the Python bindings from R
    instead.

## Scope

The comparison focuses on **graph representation and analysis**: which graph
classes each package can represent, which structural and causal-graph algorithms
it implements, and how it interoperates with other tooling.

Two adjacent areas are intentionally out of scope:

- **Causal discovery** (PC, FCI, GES, GFCI, LiNGAM, etc.). `caugi` does not
  implement discovery algorithms.
- **Statistical inference and parameter learning** (CPT estimation, likelihood,
  parameter fitting): also out of scope for `caugi`.

## Graph Types Supported

  | Package     | DAG                 | CPDAG             | MPDAG            | MAG            | PAG            | ADMG                 | SWIG | UG              | Mixed/general      |
  | :---------- | :------------------ | :---------------- | :--------------- | :------------- | :------------- | :------------------- | :--- | :-------------- | :----------------- |
  | **caugi**   | ●                   | ●[^caugi-cpdag]   | ●                | ◐[^caugi-mag]  | ○              | ●                    | ○    | ●               | ●                  |
  | igraph      | ◐[^igraph-dag]      | ○[^igraph-cpdag]  | ○                | ○              | ○              | ○                    | ○    | ◐[^igraph-ug]   | ◐[^igraph-mixed]   |
  | graph       | ◐[^graph-dag]       | ○                 | ○                | ○              | ○              | ○                    | ○    | ◐[^graph-ug]    | ◐[^graph-mixed]    |
  | gRbase      | ◐[^grbase-dag]      | ○                 | ○                | ○              | ○              | ○                    | ○    | ◐[^grbase-ug]   | ○                  |
  | pcalg       | ◐[^pcalg-dag]       | ◐[^pcalg-cpdag]   | ◐[^pcalg-mpdag]  | ◐[^pcalg-mag]  | ◐[^pcalg-pag]  | ○                    | ○    | ○               | ○                  |
  | dagitty     | ●                   | ◐[^dagitty-cpdag] | ○                | ●              | ●              | ○                    | ○    | ○               | ○                  |
  | bnlearn     | ●                   | ◐[^bnlearn-cpdag] | ○                | ○              | ○              | ○                    | ○    | ◐[^bnlearn-ug]  | ◐[^bnlearn-mixed]  |
  | ggm         | ◐[^ggm-dag]         | ◐[^ggm-cpdag]     | ○                | ◐[^ggm-mag]    | ○              | ◐[^ggm-admg]         | ○    | ◐[^ggm-ug]      | ◐[^ggm-mixed]      |
  | MixedGraphs | ◐[^mixedgraphs-dag] | ○                 | ○                | ○              | ○              | ◐[^mixedgraphs-admg] | ○    | ●               | ●                  |
  | NetworkX    | ◐[^networkx-dag]    | ○                 | ○                | ○              | ○              | ○                    | ○    | ◐[^networkx-ug] | ◐[^networkx-mixed] |
  | pgmpy       | ●                   | ◐[^pgmpy-cpdag]   | ○[^pgmpy-mpdag]  | ●              | ○              | ●                    | ○    | ●               | ○[^pgmpy-mixed]    |
  | Tetrad      | ●                   | ◐[^tetrad-cpdag]  | ◐[^tetrad-mpdag] | ◐[^tetrad-mag] | ●[^tetrad-pag] | ◐[^tetrad-admg]      | ○    | ◐[^tetrad-ug]   | ◐[^tetrad-mixed]   |

  : Supported graph types for the packages in this comparison. `●` indicates a
    dedicated class with type-level invariants; `◐` indicates representability
    without a dedicated class or invariant enforcement; `○` indicates
    unsupported graph types.

[^caugi-cpdag]: `caugi` has a dedicated `CPDAG` class whose invariants (chordal
    chain components, an acyclic component DAG, Meek closure, and strong arrow
    protection) are validated at construction via `class = "CPDAG"` (in `caugi()`,
    `as_caugi()`, and `mutate_caugi()`). `generate_graph(class = "CPDAG")`
    returns a graph typed `"CPDAG"`, and `is_cpdag()` also validates the
    structure post-hoc for graphs of other classes.

[^caugi-mag]: `caugi` accepts `class = "AG"` (ancestral graph) in the
    constructor and exports `is_mag()` to validate maximality post-hoc, but
    there is no dedicated MAG class---MAGs are representable as AGs and checked
    rather than enforced.

[^igraph-dag]: igraph has no dedicated DAG class. `is_dag()` tests whether a
    directed graph is acyclic, but the package's single `igraph` object class
    does not enforce DAG invariants. A directed graph that happens to be acyclic
    is representable, but the type is not distinguished at the object level.

[^igraph-cpdag]: igraph has no CPDAG, MPDAG, MAG, PAG, ADMG, or SWIG class and
    no associated type-level operations (confirmed absent from R source and
    NAMESPACE).

[^igraph-ug]: Undirected graphs are representable via `is_directed = FALSE`
    (e.g. `make_empty_graph(directed = FALSE)`), but there is no dedicated UG
    class enforcing undirectedness as a type invariant distinct from the general
    `igraph` object.

[^igraph-mixed]: igraph supports per-edge attributes but has no dedicated
    mixed-edge class with typed semantics. Representing a true mixed graph
    requires manual attribute manipulation with no type enforcement.

[^graph-dag]: `graph` has no dedicated DAG class. `graphNEL` and `graphAM` can
    hold directed graphs (via `edgemode = "directed"`) that happen to be
    acyclic, but the package enforces no acyclicity invariants and provides no
    DAG-specific construction or validation.

[^graph-ug]: No dedicated UG class. Undirected graphs are represented by setting
    `edgemode = "undirected"` on a general `graphNEL` / `graphAM` object; the
    package stores reciprocal edges internally but does not expose a named UG
    class.

[^graph-mixed]: `MultiGraph` holds a shared node set with multiple edge sets,
    each independently directed or undirected. It does not support a single edge
    that is both directed and bidirected, and is not designed around causal
    mixed-edge semantics (e.g. MAG/PAG/ADMG).

[^grbase-dag]: gRbase provides a `dag()` constructor and `is_dag()` type-check,
    with the underlying representation being an igraph object or adjacency
    matrix. There is no dedicated DAG class enforcing acyclicity invariants at
    the object level.

[^grbase-ug]: Same reasoning as `[^grbase-dag]`: `ug()` constructs an undirected
    graph and `is_ug()` tests for it, but the result is a plain igraph or
    matrix.

[^pcalg-dag]: pcalg produces DAGs (via `randomDAG()` / `randDAG()` returning
    `graphNEL` objects, or the `ParDAG` / `GaussParDAG` class), and
    `isValidGraph(amat, type = "dag")` validates them. The dominant user-facing
    representation is an untyped `graphNEL` rather than a pcalg-owned DAG class.

[^pcalg-cpdag]: `dag2cpdag()` produces a CPDAG and
    `isValidGraph(amat, type = "cpdag")` validates one; `pcAlgo` objects carry a
    graph with attribute `"amat.cpdag"`. There is no dedicated exported CPDAG
    class.

[^pcalg-mpdag]: `addBgKnowledge()` augments a CPDAG with background knowledge
    and re-applies Meek's rules to produce an MPDAG. As with CPDAG, no dedicated
    exported MPDAG class exists; the object is a bare adjacency matrix with
    convention-level typing only.

[^pcalg-mag]: MAG-type adjacency matrices (0/2/3 coding from Richardson &
    Spirtes 2002) are used throughout pcalg (e.g. `dsepAM()`, `backdoor()`,
    `adjustment(amat.type = "mag")`, `pcalg2dagitty()`), but there is no
    exported MAG class.

[^pcalg-pag]: `fciAlgo` objects hold a PAG as an adjacency matrix in the `@amat`
    slot. `fci()` / `rfci()` / `fciPlus()` / `dag2pag()` all return `fciAlgo`.
    The class is exported but is a *result* container for FCI-family algorithms,
    not a freely constructable PAG class.

[^dagitty-cpdag]: `dagitty` recognises only `"dag"`, `"pdag"`, `"mag"`, `"pag"`
    via `graphType()`. `equivalenceClass()` calls `dagToCpdag` internally and is
    documented as producing a CPDAG, but the result is typed `"pdag"`---no
    distinct CPDAG class or invariant enforcement. MPDAG is likewise
    representable as `"pdag"` without Meek-completion logic. `"ug"`, `"admg"`,
    `"swig"` are absent from both the R API and the JS engine.

[^bnlearn-cpdag]: `cpdag()` computes the CPDAG of a DAG and `valid.cpdag()`
    validates structure, but bnlearn uses a single generic `bn` class for all
    graph types; no dedicated CPDAG class exists and CPDAG invariants are not
    enforced at the object level.

[^bnlearn-ug]: `skeleton()` produces a fully undirected graph and `valid.ug()`
    validates whether a `bn` object is completely undirected, but the result is
    still a plain `bn` object---no dedicated UG class with enforced undirected
    invariants exists.

[^bnlearn-mixed]: A `bn` object can hold a mix of directed and undirected arcs
    (representing a PDAG), making mixed graphs implicitly representable, but
    there is no explicit untyped mixed-edge graph class distinct from the PDAG
    interpretation.

[^ggm-dag]: `ggm` represents DAGs as plain integer adjacency matrices (entries
    0, 1, 10, 100) built with `DAG()`. There is no S3/S4 class enforcing DAG
    invariants; `isAcyclic()` must be called separately.

[^ggm-cpdag]: `essentialGraph()` converts a DAG to its essential graph (=
    CPDAG), returning an adjacency matrix. There is no dedicated CPDAG class;
    the result is an untagged matrix.

[^ggm-mag]: `MAG()` derives a maximal ancestral graph after
    marginalisation/conditioning, and `isAG()` validates ancestral-graph
    conditions. The representation is still an untagged adjacency matrix.

[^ggm-admg]: `isADMG()` checks whether an adjacency matrix satisfies ADMG
    conditions. No dedicated ADMG constructor exists; the matrix encoding is the
    same general integer format used for all mixed graphs.

[^ggm-ug]: Undirected graphs are built with `UG()`, which produces an adjacency
    matrix with edge-type value 10. No distinct UG class enforces undirected
    invariants.

[^ggm-mixed]: `makeMG()` combines directed, undirected, and bidirected
    components into one adjacency matrix; `grMAT()` converts graphNEL/igraph
    inputs to this format. The representation is a general integer adjacency
    matrix with no commitment to a specific typed mixed-graph semantics.

[^mixedgraphs-dag]: `MixedGraphs` has no dedicated DAG class. `is_DAG()` checks
    that a `mixedgraph` carries only directed edges and is acyclic, but the
    constructor does not enforce DAG invariants. The `mixedgraph` object is an
    untyped container.

[^mixedgraphs-admg]: `is_ADMG()` tests whether a `mixedgraph` contains only
    directed and bidirected edges and is acyclic, but there is no dedicated ADMG
    class. ADMG-typed objects from the companion `ADMGs` package can be
    converted to/from `mixedgraph` via `convert()`, but `MixedGraphs` itself
    does not expose an ADMG class.

[^networkx-dag]: NetworkX's `DiGraph` class stores directed edges and supports
    DAG-related algorithms (`is_directed_acyclic_graph`, `topological_sort`,
    `ancestors`, `descendants`), but the class itself does not enforce
    acyclicity as an invariant---a `DiGraph` can hold cycles. There is no
    dedicated `DAG` subclass.

[^networkx-ug]: NetworkX's `Graph` class stores undirected edges, making it
    suitable for UGs, but it carries no type-level semantic enforcement (no
    bidirected edges, no directed edges, etc.).

[^networkx-mixed]: NetworkX has no mixed-edge graph class. Its four classes
    (`Graph`, `DiGraph`, `MultiGraph`, `MultiDiGraph`) are all homogeneous-edge.
    The `pywhy-graphs` extension provides mixed-graph types but is a separate
    package.

[^pgmpy-cpdag]: `pgmpy` has a single `PDAG` class (`pgmpy.base.PDAG`) documented
    as "also known as CPDAG". It supports CPDAG construction from a DAG via
    `DAG.to_pdag()` (Chickering 2002) and `to_cpdag()` / `apply_meeks_rules()`
    methods. There is no separate dedicated CPDAG class, and PDAG does not
    enforce the invariant that the graph represents a complete Markov
    equivalence class.

[^pgmpy-mpdag]: No dedicated MPDAG class.
    `PDAG.apply_meeks_rules(apply_r4 = True)` applies all four Meek rules and
    could produce an MPDAG, but there is no type-specific MPDAG class or
    MPDAG-specific construction pathway.

[^pgmpy-mixed]: `AncestralBase` stores mixed-edge graphs with arbitrary marks
    and is the parent of `MAG`. It is an internal base class, not a user-facing
    untyped mixed-edge graph class. Coverage of several typed mixed graphs
    (ADMG, MAG, PDAG) does not by itself satisfy this column.

[^tetrad-cpdag]: Tetrad has no dedicated `Cpdag` class. CPDAGs are represented
    as `EdgeListGraph` objects containing directed and undirected edges. The
    library validates CPDAG status at runtime via `Paths.isLegalCpdag()` and
    produces CPDAGs through `GraphTransforms.dagToCpdag()` /
    `GraphSearchUtils.basicCpdag()`. The CPDAG concept is operationally
    first-class but not a distinct enforced type.

[^tetrad-mpdag]: No dedicated `Mpdag` class. Tetrad validates MPAGs via
    `Paths.isLegalMpag()`, and `MeekRules.orientImplied(graph)` with a populated
    `Knowledge` object applies Meek rules under background constraints, but
    there is no single `dagToMpdag` factory.

[^tetrad-mag]: No dedicated `Mag` class. MAGs are represented as generic
    `EdgeListGraph` objects and validated post-hoc via `Paths.isLegalMag()`.
    Constructed through `GraphTransforms.dagToMag(dag)` and converted to PAGs
    via `GraphTransforms.magToPag()`.

[^tetrad-pag]: No dedicated `Pag` class. PAGs are `EdgeListGraph` instances with
    circle, tail, and arrowhead endpoints. Validated via `Paths.isLegalPag()`.
    Produced by `GraphTransforms.dagToPag()` and `GraphTransforms.magToPag()`.

[^tetrad-admg]: `SemGraph` (in `edu.cmu.tetrad.graph`) enforces directed +
    bidirected edges only and automatically manages error nodes for each
    endogenous variable, implementing ADMG-like semantics. However, it is scoped
    to structural-equation-model graphs (with explicit error nodes) rather than
    a general-purpose ADMG class.

[^tetrad-ug]: No dedicated undirected-graph class. Undirected edges can be added
    to `EdgeListGraph` via `addUndirectedEdge()`, and
    `GraphUtils.undirectedGraph(g)` converts all edges to undirected. There is
    no enforced `UG` type with invariant-checking.

[^tetrad-mixed]: `EdgeListGraph` is the universal mixed-edge container and
    accepts all five endpoint combinations (directed, undirected, bidirected,
    partially-oriented, nondirected). It does not commit to any single graph
    semantics, but it is not branded as a distinct "mixed/general" class---it is
    simply the default implementation of the `Graph` interface.

## Graph Queries and Structural Operations

  | Package     | Parents/children   | Ancestors/descendants | d-sep             | m-sep          | Paths           | Acyclicity              | Markov blanket     | Moralization | Skeleton              | v-structures            | MEC enumeration |
  | :---------- | :----------------- | :-------------------- | :---------------- | :------------- | :-------------- | :---------------------- | :----------------- | :----------- | :-------------------- | :---------------------- | :-------------- |
  | **caugi**   | ●                  | ●                     | ●                 | ●              | ○               | ●                       | ●                  | ●            | ●                     | ○                       | ○               |
  | igraph      | ◐[^igraph-parents] | ◐[^igraph-ancestors]  | ○                 | ○              | ●               | ●                       | ○                  | ○            | ○                     | ○                       | ○               |
  | graph       | ◐[^graph-parents]  | ◐[^graph-ancestors]   | ○                 | ○              | ◐[^graph-paths] | ○                       | ○                  | ○            | ○                     | ○                       | ○               |
  | gRbase      | ◐[^grbase-par-ch]  | ◐[^grbase-anc]        | ○                 | ○              | ○               | ●                       | ○                  | ●            | ○                     | ○                       | ○               |
  | pcalg       | ◐[^pcalg-par-ch]   | ◐[^pcalg-anc-de]      | ◐[^pcalg-dsep]    | ◐[^pcalg-msep] | ○               | ◐[^pcalg-acyc]          | ○                  | ○            | ◐[^pcalg-skel]        | ◐[^pcalg-vstruct]       | ●               |
  | dagitty     | ●                  | ●                     | ●                 | ○              | ●               | ●                       | ●                  | ●            | ○                     | ◐[^dagitty-vstructures] | ●               |
  | bnlearn     | ●                  | ●                     | ◐[^bnlearn-dsep]  | ○              | ●               | ●                       | ●                  | ●            | ●                     | ●                       | ●               |
  | ggm         | ◐[^ggm-pa-ch]      | ◐[^ggm-anc]           | ●                 | ●              | ◐[^ggm-paths]   | ●                       | ○                  | ○            | ○                     | ○                       | ◐[^ggm-mec]     |
  | MixedGraphs | ●                  | ●                     | ○                 | ●              | ○               | ◐[^mixedgraphs-acyclic] | ◐[^mixedgraphs-mb] | ●            | ●                     | ○                       | ○               |
  | NetworkX    | ◐[^networkx-par]   | ●                     | ◐[^networkx-dsep] | ○              | ●               | ●                       | ○                  | ●            | ◐[^networkx-skeleton] | ●                       | ○               |
  | pgmpy       | ●                  | ◐[^pgmpy-desc]        | ●                 | ●              | ○               | ●                       | ●                  | ●            | ○[^pgmpy-skel]        | ●                       | ○               |
  | Tetrad      | ●                  | ●                     | ◐[^tetrad-dsep]   | ●              | ●               | ◐[^tetrad-acyclic]      | ●                  | ○            | ◐[^tetrad-skeleton]   | ◐[^tetrad-vstructs]     | ●               |

  : Overview of available graph queries and structural operations in the
    packages in the comparison.

[^igraph-parents]: Parents and children are obtainable via
    `neighbors(graph, v, mode = "in" / "out")`, but there are no dedicated
    `parents()` / `children()` exported functions.

[^igraph-ancestors]: Ancestors and descendants are obtainable via
    `subcomponent(graph, v, mode = "in" / "out")`, but there are no dedicated
    `ancestors()` / `descendants()` exported functions.

[^graph-parents]: No dedicated `parents()` or `children()` functions.
    `inEdges()` returns the set of in-neighbours (functional equivalent of
    parents on a directed graph); `edges()` returns out-neighbours (functional
    equivalent of children). Both are exported S4 generics.

[^graph-ancestors]: `acc()` returns all nodes reachable from a given node on a
    directed graph, equivalent to the descendant set. There is no `ancestors()`
    equivalent exported by the package.

[^graph-paths]: `DFS()` (exported) provides depth-first graph traversal;
    `pathWeights()` computes weights along a user-specified path. No all-paths
    enumeration, shortest-path, or directed-path-existence function is provided;
    those require the companion `RBGL` package.

[^grbase-par-ch]: `parents()` and `children()` are exported
    (`R/graph-querygraph.R`). There is no corresponding `descendants()` function
    in the exported API; `ancestors()` / `ancestralSet()` / `ancestralGraph()`
    cover the ancestor side but the symmetric descendent query is absent.

[^grbase-anc]: `ancestors()`, `ancestralSet()`, and `ancestralGraph()` are
    exported. A dedicated `descendants()` is absent from the exported API.

[^pcalg-par-ch]: `searchAM(amat, x, type = "pa" / "ch")` returns parents and
    children for DAG, CPDAG, MAG, and PAG adjacency matrices. There is no
    standalone `parents()` or `children()` function; retrieval goes through the
    general `searchAM()` dispatcher.

[^pcalg-anc-de]: `searchAM(amat, x, type = "an" / "de")` returns ancestors and
    descendants for DAG/CPDAG/MAG/PAG. `possAn()` and `possDe()` /
    `possibleDe()` return possible ancestors/descendants in PAGs. All operate on
    raw adjacency matrices via `searchAM()`.

[^pcalg-dsep]: `dsep(a, b, S, g)` tests d-separation in a DAG (using a
    `graphNEL` object and moralization; Lauritzen 2004). `dsepTest()` wraps it
    for use as a CI oracle. There is no exported d-separation function for
    CPDAGs or PDAGs directly; `dsep()` is DAG-only.

[^pcalg-msep]: `dsepAM(X, Y, S, amat)` tests m-separation on MAG adjacency
    matrices. `dsepAMTest()` wraps it for algorithmic use. Coverage is MAG-only;
    there is no exported m-separation function for PAGs in pcalg's own API.

[^pcalg-acyc]: `isValidGraph(amat, type = "dag")` checks for directed cycles as
    part of DAG validation. There is no standalone `is_acyclic()` function;
    acyclicity is only surfaced through `isValidGraph()`.

[^pcalg-skel]: The `skeleton()` function *estimates* a skeleton from data using
    conditional independence tests (it is a causal discovery function). There is
    no separate exported function to extract or query the skeleton of an
    already-known graph.

[^pcalg-vstruct]: `udag2pdag()` and `dag2cpdag()` identify v-structures as part
    of the CPDAG-completion process, but there is no standalone exported
    function to enumerate or check v-structures of a given graph.

[^dagitty-vstructures]: No function enumerates v-structures as a set.
    `isCollider(x, u, v, w)` tests a single triple, but there is no exported
    `vStructures()` or equivalent. Skeleton (undirected version of the graph) is
    also absent as a dedicated function.

[^bnlearn-dsep]: `dsep()` tests d-separation on `bn` objects. When the input
    contains undirected arcs (PDAG/CPDAG), the function calls
    `cpdag.extension()` to produce a single consistent DAG extension first;
    d-separation is then tested on that DAG rather than natively on the PDAG.

[^ggm-pa-ch]: `pa()` returns parents and `ch()` returns children of a node set
    in a DAG. `bd()` returns the boundary (neighbours + parents). These operate
    on raw adjacency matrices and are undocumented for MAG/mixed-graph inputs;
    no `descendants()` or `spouses()` analogue is exported.

[^ggm-anc]: `ancGraph()` / `anGraph()` compute the transitive closure (ancestor
    relation) of a DAG as a Boolean adjacency matrix rather than returning a
    node list. No `descendants()` function is exported.

[^ggm-paths]: `findPath()` finds a single path between two nodes in an
    undirected graph (used internally by `fundCycles()`); it is not intended for
    direct user calls and does not enumerate all paths.

[^ggm-mec]: `essentialGraph()` returns the CPDAG (encoding the full MEC) but
    does not enumerate the Markov-equivalent DAGs. `MarkEqRcg()` / `MarkEqMag()`
    test pairwise Markov equivalence; `RepMarDAG()` / `RepMarUG()` /
    `RepMarBG()` find a single representative.

[^mixedgraphs-acyclic]: `is_cyclic()` is exported and checks directed cycles,
    and `topologicalOrder()` / `isTopological()` implicitly validate acyclicity,
    but `is_cyclic()` documentation notes it is "Not tested"---`◐` reflects this
    caveat.

[^mixedgraphs-mb]: `mb()` finds the "Markov blanket for a vertex in an ancestral
    set" but requires the user to supply an explicit ancestral set `A` and the
    vertex must be childless in that set---a constrained helper rather than a
    standard graph-wide Markov-blanket function.

[^networkx-par]: `DiGraph.predecessors()` and `DiGraph.successors()` return
    parent/child iterators. There is no dedicated `parents()` / `children()` API
    for a named causal DAG.

[^networkx-dsep]: `networkx.algorithms.d_separation` provides `is_d_separator`,
    `is_minimal_d_separator`, and `find_minimal_d_separator`. All three require
    a DAG and raise `NetworkXError` on cyclic or undirected graphs. They do not
    support PDAGs, MAGs, or any other mixed-edge graph type.

[^networkx-skeleton]: NetworkX has no dedicated `skeleton()` function. The moral
    graph (`nx.moral_graph`) is available, but skeleton extraction (dropping
    directionality without moralization) is not an exported function.

[^pgmpy-desc]: `DAG.get_ancestors()` is implemented. There is no
    `DAG.get_descendants()` method; `nx.descendants()` from NetworkX can be
    called on the underlying `nx.DiGraph` directly, but it is not part of
    pgmpy's exported API. `ADMG` does provide both `get_ancestors()` and
    `get_descendants()`.

[^pgmpy-skel]: No `skeleton()` method exists in any pgmpy graph class. The
    undirected skeleton is accessible via the inherited
    `nx.DiGraph.to_undirected()`, but this is not a pgmpy-level exported
    function.

[^tetrad-dsep]: `Paths.isMSeparatedFrom(x, y, z, isPag)` implements
    d-/m-separation. The simpler overload's Javadoc says "DAG only", yet the
    `isPag` parameter explicitly switches PAG semantics. Per-graph-type
    documentation is inconsistent; users access d-separation as
    `isMSeparatedFrom(..., false)`. The back-door helper
    `Paths.isSatisfyBackDoorCriterion()` constructs a `Dag` internally and
    throws if input is not a DAG.

[^tetrad-acyclic]: There is no `isAcyclic()` method. Callers use
    `!graph.paths().existsDirectedCycle()` or `Paths.isLegalDag()` /
    `GraphUtils.isDag()`. Fully supported but requires negation of a
    cycle-detection predicate.

[^tetrad-skeleton]: No dedicated `skeleton()` method.
    `GraphUtils.undirectedGraph(g)` converts all edges to undirected (producing
    the skeleton graph object), but it is a workaround.

[^tetrad-vstructs]: `GraphUtils.listColliderTriples(graph)` returns all definite
    collider triples. However, `EdgeListGraph.isDefCollider()` checks arrowhead
    endpoints without testing non-adjacency of the outer nodes, so the method
    returns both shielded and unshielded colliders rather than v-structures
    (unshielded colliders) exclusively.

## Causal-Inference Algorithms

  | Package     | Back-door adj.       | Generalized adj. | Optimal adj.       | ID algorithm | Do-calculus | Counterfactuals | Interventions/mutilation |
  | :---------- | :------------------- | :--------------- | :----------------- | :----------- | :---------- | :-------------- | :----------------------- |
  | **caugi**   | ●                    | ●                | ◐[^caugi-optimal]  | ○            | ○           | ○               | ○                        |
  | igraph      | ○                    | ○                | ○                  | ○            | ○           | ○               | ○                        |
  | graph       | ○                    | ○                | ○                  | ○            | ○           | ○               | ○                        |
  | gRbase      | ○                    | ○                | ○                  | ○            | ○           | ○               | ○                        |
  | pcalg       | ◐[^pcalg-backdoor]   | ●                | ●                  | ○            | ○           | ○               | ◐[^pcalg-ivent]          |
  | dagitty     | ◐[^dagitty-backdoor] | ○                | ○                  | ○            | ○           | ○               | ◐[^dagitty-mutilate]     |
  | bnlearn     | ○                    | ○                | ○                  | ○            | ○           | ●               | ●                        |
  | ggm         | ○                    | ○                | ○                  | ○            | ○           | ○               | ○                        |
  | MixedGraphs | ○                    | ○                | ○                  | ○            | ○           | ○               | ◐[^mixedgraphs-mutilate] |
  | NetworkX    | ○                    | ○                | ○                  | ○            | ○           | ○               | ○                        |
  | pgmpy       | ●                    | ●                | ○                  | ○            | ○           | ○               | ●                        |
  | Tetrad      | ◐[^tetrad-backdoor]  | ●                | ◐[^tetrad-optimal] | ○            | ○           | ○               | ○                        |

  : Overview of available causal-inference algorithms in the packages in the
    comparison.

[^caugi-optimal]: `adjustment_set(cg, X, Y, type = "optimal")` returns the O-set
    of Henckel/Perković/Maathuis (2019), but only for DAGs---the underlying Rust
    implementation errors on non-DAG graph classes. There is no O-set support
    for CPDAGs, MPDAGs, MAGs, or PAGs.

[^pcalg-backdoor]: `backdoor(amat, x, y, type)` implements the Generalized
    Backdoor Criterion (GBC) of Maathuis & Colombo (2015), which subsumes
    Pearl's classical back-door criterion. For DAGs the GBC reduces to Pearl's
    back-door, so back-door is available as a special case, but the function
    does not specifically enforce or name the classical back-door separately.
    Generalized adjustment is `adjustment(amat, amat.type, x, y, set.type)`
    (full GAC, Perkovic et al. 2015/2018), and optimal is
    `optAdjSet(graphEst, x.pos, y.pos)` (Henckel/Perkovic/Maathuis 2019,
    DAG/CPDAG/PDAG only).

[^pcalg-ivent]: `rmvnorm.ivent(n, object, target, target.value)` simulates from
    an interventional distribution of a `GaussParDAG` by fixing target nodes ---
    implements the do-operator semantics within the simulation framework but
    does not mutilate the graph object itself. There is no exported `mutilate()`
    or `do()` helper that returns a modified graph with incoming edges removed
    from intervention nodes.

[^dagitty-backdoor]: `adjustmentSets(effect = "total")` and `isAdjustmentSet()`
    both invoke `GraphTransformer.backDoorGraph()` in the JS engine---Pearl's
    back-door graph construction, not the generalized Perkovic criterion. The R
    documentation cites Perkovic et al. (2015), but the JS implementation
    (`listMsasTotalEffect` in `jslib/graph/GraphAnalyzer.js`) applies the
    back-door graph uniformly across dag/pdag/mag/pag. Generalized adjustment
    scores `○` because no separate generalized-adjustment code path exists.
    `type = "canonical"` returns the ancestor-based canonical set but is not an
    optimality criterion in the Rotnitzky/Henckel sense.

[^dagitty-mutilate]: `backDoorGraph(x)` removes the first directed edge on every
    proper causal path (with PAG handling via `pagToPdag` conversion)---the
    back-door-graph helper, not a general `do()`-style mutilation that removes
    all incoming edges to an arbitrary intervention set. No `mutilate()` or
    `do()` function is exported.

[^mixedgraphs-mutilate]: `mutilate()` removes edges adjacent to a specified
    vertex set and supports a `dir = -1` option that removes only incoming
    edges. It can mechanically produce the mutilated graph required by `do()`,
    but the function is documented as a general edge-deletion utility with no
    causal semantics, no treatment of bidirected edges in the do-calculus sense,
    and no convenience wrapper that enforces "remove all incoming edges to the
    intervention set."

[^tetrad-backdoor]: `Paths.isSatisfyBackDoorCriterion(graph, x, y, z)`
    implements Pearl's back-door criterion check, but the implementation
    constructs `new Dag(graph)` internally and throws if the input is not a DAG.
    Back-door adjustment *sets* are not separately enumerated; the generalized
    `RecursiveAdjustment.adjustmentSets()` subsumes the back-door case for DAGs.

[^tetrad-optimal]: `OSet.oSetDag(graph, X, Y)` and
    `OSet.oSetCpdag(graph, X, Y, maxPathLength)` implement the
    Henckel--Perković--Maathuis O-set for DAGs and (amenable) CPDAGs
    respectively. The `Ida` and `PdagPagIda` classes wrap this as
    `IDA_TYPE.OPTIMAL`. Coverage is restricted to DAG/CPDAG; O-sets for
    MAGs/PAGs are not implemented.

## I/O and Interoperability

  | Package     | DOT              | Mermaid | GraphML | JSON           | Coerce to/from other graph classes |
  | :---------- | :--------------- | :------ | :------ | :------------- | :--------------------------------- |
  | **caugi**   | ◐[^caugi-dot]    | ●       | ●       | ●[^caugi-json] | ●                                  |
  | igraph      | ◐[^igraph-dot]   | ○       | ●       | ○              | ◐[^igraph-coerce]                  |
  | graph       | ◐[^graph-dot]    | ○       | ○       | ○              | ◐[^graph-coerce]                   |
  | gRbase      | ○                | ○       | ○       | ○              | ◐[^grbase-coerce]                  |
  | pcalg       | ○                | ○       | ○       | ○              | ◐[^pcalg-coerce]                   |
  | dagitty     | ○                | ○       | ○       | ○              | ◐[^dagitty-coerce]                 |
  | bnlearn     | ◐[^bnlearn-dot]  | ○       | ○       | ○              | ●                                  |
  | ggm         | ○                | ○       | ○       | ○              | ◐[^ggm-coerce]                     |
  | MixedGraphs | ○                | ○       | ○       | ○              | ●                                  |
  | NetworkX    | ◐[^networkx-dot] | ○       | ●       | ●              | ◐[^networkx-coerce]                |
  | pgmpy       | ◐[^pgmpy-dot]    | ○       | ○       | ○              | ◐[^pgmpy-coerce]                   |
  | Tetrad      | ◐[^tetrad-dot]   | ○       | ○       | ●              | ○                                  |

  : Comparison of supported graph I/O formats and interoperability features.

[^caugi-dot]: `to_dot()` / `write_dot()` write Graphviz DOT format, but caugi
    has no DOT reader---round-trip via DOT is not supported.

[^caugi-json]: `caugi_serialize()` / `caugi_deserialize()` (and `read_caugi()` /
    `write_caugi()`) read and write caugi's own JSON-based serialization format
    (Rust-side: `src/rust/src/lib.rs`). It is not a standardized cross-package
    JSON schema.

[^igraph-dot]: DOT (Graphviz) format is supported for writing only
    (`write_graph(format = "dot")`). Reading DOT format is not supported.
    `read_graph()` accepts: edgelist, pajek, ncol, lgl, graphml, dimacs,
    graphdb, gml, dl.

[^igraph-coerce]: igraph provides explicit coercion to/from `graphNEL` objects
    from the Bioconductor `graph` package via `as_graphnel()` and
    `graph_from_graphnel()`. No coercion helpers exist for dagitty, bnlearn,
    pcalg, or other causal R package object classes.

[^graph-dot]: Write only. `toDotWithRI()` and `toDotR()` serialise `graphNEL`
    objects to DOT (Graphviz) format. No DOT reader is provided in the `graph`
    package.

[^graph-coerce]: Internal coercions are provided: `as(graphNEL, "graphAM")`,
    `as(graphAM, "graphNEL")`, `as(graphBAM, "graphNEL")`,
    `as(matrix, "graphNEL")`, `as(matrix, "graphAM")`, `graph2SparseM()` /
    `sparseM2Graph()` (requiring optional `SparseM`). No coercions to or from
    `igraph`, `dagitty`, or `bnlearn` objects are exported by `graph` itself.

[^grbase-coerce]: gRbase provides `as()` S4 coercion methods that convert among
    igraph, graphNEL (Bioconductor `graph`), dense adjacency matrix, and sparse
    `dgCMatrix` representations. There are no explicit coercion helpers for
    other *causal-graph* packages (dagitty, bnlearn, pcalg).

[^pcalg-coerce]: `pcAlgo` and `fciAlgo` objects can be coerced to `graphAM` /
    `graphNEL` (from `graph`) via S4 coercion methods. The package wraps
    `graphNEL` as its primary DAG container, and `iplotPC()` converts to
    `igraph` for plotting. Incoming coercion (from igraph, bnlearn, dagitty,
    etc. into pcalg objects) is not provided.

[^dagitty-coerce]: `convert(x, to = "igraph")` and
    `convert(x, to = "causaleffect")` produce igraph objects (one-way: dagitty →
    igraph only). `as.dagitty.bn()` converts bnlearn `bn` objects to dagitty
    (one-way the other direction). `lavaanToGraph()` converts lavaan models to
    dagitty. No coercion to/from `graphNEL`, `pcalg`, or other classes.

[^bnlearn-dot]: `write.dot()` exports the network structure to Graphviz DOT
    format, but there is no corresponding `read.dot()`---DOT support is
    write-only.

[^ggm-coerce]: `grMAT()` converts graphNEL (from `graph`) and igraph objects
    *into* ggm adjacency matrices. `plotGraph()` also accepts graphNEL/igraph as
    input. There is no function to export a ggm adjacency matrix back to a
    graphNEL or igraph object; coercion is one-directional (into ggm's format).

[^networkx-dot]: DOT format read (`read_dot`) and write (`write_dot`) are
    available via `networkx.drawing.nx_pydot`, but `pydot` is an optional
    external dependency that must be installed separately. A pending deprecation
    warning has also been filed for `nx_pydot` (issue #5723).

[^networkx-coerce]: NetworkX provides explicit conversion helpers to/from NumPy
    arrays, SciPy sparse arrays, and Pandas DataFrames. Conversion to/from other
    *graph* objects (igraph, graph-tool) is handled by those packages' own
    `from_networkx()` / `to_networkx()` methods, not by NetworkX itself.

[^pgmpy-dot]: `DAG.to_graphviz()` returns a `pygraphviz.AGraph` from which DOT
    text can be obtained via `.to_string()`. There is no direct `to_dot()` /
    `from_dot()` API; reading DOT is not supported through pgmpy's own API.

[^pgmpy-coerce]: pgmpy graph classes (`DAG`, `PDAG`, `ADMG`) inherit from
    `networkx.DiGraph`/`networkx.MultiDiGraph`, so they are directly usable as
    NetworkX objects. There are no explicit coercion helpers to or from other
    graph-package objects (igraph, R dagitty objects, bnlearn objects).

[^tetrad-dot]: `GraphSaveLoadUtils.graphToDot(graph)` writes Graphviz DOT format
    (string and file variants). There is no DOT reader, so round-trip is not
    supported. Write-only.

## Implementation and Ergonomics

  | Package     | Backend           | Sparse storage         | Built-in plotting    | Layout algorithms  | Pipeable/fluent API  | Lazy mutation |
  | :---------- | :---------------- | :--------------------- | :------------------- | :----------------- | :------------------- | :------------ |
  | **caugi**   | R + Rust          | ●                      | ●                    | ●                  | ●                    | ●             |
  | igraph      | C                 | ◐[^igraph-sparse]      | ●                    | ●                  | ○                    | ○             |
  | graph       | R + C             | ●[^graph-sparse]       | ○[^graph-plot]       | ○                  | ○                    | ○             |
  | gRbase      | R + C++           | ●                      | ◐[^grbase-plot]      | ○                  | ○                    | ○             |
  | pcalg       | R + C++           | ○                      | ◐[^pcalg-plot]       | ○                  | ○                    | ○             |
  | dagitty     | R + JS            | ○                      | ●                    | ◐[^dagitty-layout] | ○                    | ○             |
  | bnlearn     | R + C             | ◐[^bnlearn-sparse]     | ◐[^bnlearn-plot]     | ◐[^bnlearn-layout] | ○                    | ○             |
  | ggm         | R                 | ○                      | ●                    | ◐[^ggm-layout]     | ○                    | ○             |
  | MixedGraphs | R + C++           | ◐[^mixedgraphs-sparse] | ◐[^mixedgraphs-plot] | ○                  | ◐[^mixedgraphs-pipe] | ○             |
  | NetworkX    | Python            | ◐[^networkx-sparse]    | ◐[^networkx-plot]    | ●                  | ○                    | ○             |
  | pgmpy       | Python (NetworkX) | ○                      | ◐[^pgmpy-plot]       | ◐[^pgmpy-layout]   | ○                    | ○             |
  | Tetrad      | Java              | ●                      | ◐[^tetrad-plotting]  | ●                  | ○                    | ○             |

  : Comparison of implementation details and ergonomic features of the packages
    in the comparison.

[^igraph-sparse]: The C igraph library uses adjacency-list storage internally.
    On the R side, `as_adjacency_matrix(sparse = TRUE)` and
    `graph_from_adjacency_matrix()` accept/return sparse `Matrix` objects
    (`dgCMatrix`), but the igraph object itself is an opaque external pointer
    backed by the C library's adjacency-list representation, not an R-level
    sparse matrix.

[^graph-sparse]: `graphNEL` uses an adjacency-list representation (sparse by
    nature). `graphBAM` (experimental) stores adjacency as a compact bit-array.
    `graph2SparseM()` / `sparseM2Graph()` convert to/from `SparseM` sparse
    matrices when that package is installed.

[^graph-plot]: No built-in plotting. DOT serialisation helpers (`toDotWithRI`,
    `toDotR`) are provided, but actual rendering requires the separate
    `Rgraphviz` package. No layout algorithms are implemented in `graph` itself.

[^grbase-plot]: gRbase exports `iplot()` / `graph-iplot`, a thin wrapper around
    igraph's `plot.igraph()`. There are no built-in layout algorithms: layout
    selection is delegated entirely to igraph.

[^pcalg-plot]: `pcAlgo` and `fciAlgo` objects have `plot()` S4 methods that
    delegate to `Rgraphviz`. `iplotPC()` delegates to `igraph`'s plot.
    `plotSG()` also requires `Rgraphviz`. There are no native layout algorithms
    in pcalg itself.

[^dagitty-layout]: `graphLayout(x, method = "spring")` implements a single
    spring/force-directed layout algorithm. The documentation explicitly states
    `"currently, only 'spring' is supported"`.

[^bnlearn-sparse]: The `bn` class stores arcs as a 2-column character matrix
    (`$arcs`) plus per-node children/parents lists in `$nodes`. The exported
    `alst()` function exposes an adjacency-list view. This is effectively sparse
    but is not a formal CSR/CSC or `Matrix`-class sparse type.

[^bnlearn-plot]: A rudimentary `plot.bn()` S3 method draws nodes in a circle
    with no external dependencies, but the package documentation describes it as
    "a last resort for when Rgraphviz is not available." The full-featured
    `graphviz.plot()` requires the Bioconductor `Rgraphviz` package.

[^bnlearn-layout]: All graph layout computation is delegated to Graphviz via the
    `Rgraphviz` package (algorithms: dot, neato, twopi, circo, fdp); bnlearn
    implements no native layout algorithms.

[^ggm-layout]: `plotGraph()` and `drawGraph()` delegate layout to igraph
    (default `layout.auto`), so any igraph layout function can be passed via the
    `layout` argument. The layouts are not implemented natively in ggm.

[^mixedgraphs-sparse]: Edge data can be stored in adjacency-list format
    (`adjList` class), which is a sparse representation. However, the package
    also supports dense adjacency matrices (`adjMatrix` class) and does not
    commit to a single internal format. No CSR/CSC or other documented
    sparse-matrix format is enforced.

[^mixedgraphs-plot]: `plot.mixedgraph()` is an S3 method but it delegates
    entirely to the `pcalg` / `Rgraphviz` stack by converting the graph to a
    `fciAlgo` object. Layout is determined by Rgraphviz; no layout algorithms
    are implemented within MixedGraphs. The function is listed under `Suggests`
    (`pcalg`, `Rgraphviz`), so plotting is unavailable without those packages.

[^mixedgraphs-pipe]: The package exports a `%G%` infix operator that
    automatically converts a `mixedgraph` into the format expected by the
    right-hand-side function (from another package), enabling cross-package
    chaining. This is a narrow inter-package bridge operator rather than a full
    tidyverse-style verb chain that takes and returns `mixedgraph` objects.

[^networkx-sparse]: NetworkX stores graphs internally as a
    dict-of-dicts-of-dicts (adjacency dictionary), which is *effectively* sparse
    for large, sparse graphs (only present edges are stored), but this is not a
    CSR/CSC/COO sparse matrix---it is a plain Python dictionary structure.
    Conversion to SciPy sparse arrays is available via `to_scipy_sparse_array`.

[^networkx-plot]: NetworkX includes `nx.draw()`, `nx.draw_networkx()`, and
    related functions that render graphs via Matplotlib
    (`networkx.drawing.nx_pylab`). Matplotlib is an optional dependency ---
    NetworkX raises `ImportError` when it is absent. The drawing module is
    described in the documentation as basic/non-primary functionality.

[^pgmpy-plot]: `DAG.to_graphviz()` returns a pygraphviz `AGraph` (requires the
    optional `pygraphviz` dependency); `DAG.to_daft()` returns a `daft` PGM
    object. Neither is a `plot()` method that renders to screen directly. No
    built-in `matplotlib`-based `plot()` is present.

[^pgmpy-layout]: Layout computation is delegated to pygraphviz (via
    `to_graphviz()`) or to NetworkX spring-layout variants (inside `to_daft()`).
    pgmpy does not implement its own layout algorithms.

[^tetrad-plotting]: Tetrad's GUI module (`tetrad-gui`) provides rich interactive
    graph visualization, but this is a Swing-based desktop application
    component, not a programmatic plotting API. From the library (`tetrad-lib`),
    `LayoutUtil` computes node coordinates (Kamada-Kawai, Fruchterman-Reingold,
    circle, square, causal-order layouts) and `GraphSaveLoadUtils.graphToDot()`
    exports to Graphviz for external rendering. There is no `plot(graph)` method
    in the library that renders directly to a file or screen without the GUI.

## Contributing

If you find any errors in the comparison above or would like to add another
package for comparisons, please file an issue or submit a pull request with the
relevant information.
