Skip to content

Commit

Permalink
API: dump DOT graphs by directly running GraphViz
Browse files Browse the repository at this point in the history
  • Loading branch information
johnyf committed Dec 12, 2023
1 parent 1d44159 commit 12906ed
Show file tree
Hide file tree
Showing 14 changed files with 393 additions and 200 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Contains:
- Zero-omitted binary decision diagrams (ZDDs) in CUDD
- Conversion from BDDs to MDDs.
- Conversion functions to [`networkx`](https://networkx.org) and
[`pydot`](https://pypi.org/project/pydot) graphs.
[DOT](https://www.graphviz.org/doc/info/lang.html) graphs.
- BDDs have methods to `dump` and `load` them using [JSON](
https://wikipedia.org/wiki/JSON), or [`pickle`](
https://docs.python.org/3/library/pickle.html).
Expand Down Expand Up @@ -148,7 +148,7 @@ from dd.cudd import BDD
```

Most useful functionality is available via methods of the class `BDD`.
A few of the functions can prove handy too, mainly `to_nx`, `to_pydot`.
A few of the functions can prove useful too, among them `to_nx()`.
Use the method `BDD.dump` to write a `BDD` to a `pickle` file, and
`BDD.load` to load it back. A CUDD dddmp file can be loaded using
the function `dd.dddmp.load`.
Expand Down
161 changes: 150 additions & 11 deletions dd/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
# Copyright 2017-2018 by California Institute of Technology
# All rights reserved. Licensed under 3-clause BSD.
#
import collections as _cl
import collections.abc as _abc
import os
import shlex as _sh
import subprocess as _sbp
import textwrap as _tw
import types
import typing as _ty
Expand All @@ -15,17 +18,10 @@
except ImportError as error:
_nx = None
_nx_error = error
try:
import pydot as _pydot
except ImportError as error:
_pydot = None
_pydot_error = error


if _nx is not None:
MultiDiGraph: _ty.TypeAlias = _nx.MultiDiGraph
if _pydot is not None:
Dot: _ty.TypeAlias = _pydot.Dot


# The mapping from values of argument `op` of
Expand All @@ -50,13 +46,11 @@ def import_module(
Raise `ImportError` otherwise.
"""
modules = dict(
networkx=_nx,
pydot=_pydot)
networkx=_nx)
if module_name in modules:
return modules[module_name]
errors = dict(
networkx=_nx_error,
pydot=_pydot_error)
networkx=_nx_error)
raise errors[module_name]


Expand Down Expand Up @@ -321,3 +315,148 @@ def assert_operator_arity(
if w is None:
raise ValueError(
'`w is None`')


_GraphType: _ty.TypeAlias = _ty.Literal[
'digraph',
'graph',
'subgraph']
DOT_FILE_TYPES: _ty.Final = {
'pdf', 'svg', 'png', 'dot'}


class DotGraph:
def __init__(
self,
graph_type:
_GraphType='digraph',
rank:
str |
None=None
) -> None:
"""A DOT graph."""
self.graph_type = graph_type
self.rank = rank
self.nodes = _cl.defaultdict(dict)
self.edges = _cl.defaultdict(list)
self.subgraphs = list()

def add_node(
self,
node,
**kw
) -> None:
"""Add node with attributes `kw`.
If node exists, update its attributes.
"""
self.nodes[node].update(kw)

def add_edge(
self,
start_node,
end_node,
**kw
) -> None:
"""Add edge with attributes `kw`.
Multiple edges can exist between the same nodes.
"""
self.edges[start_node, end_node].append(kw)

def to_dot(
self,
graph_type:
_GraphType |
None=None
) -> str:
"""Return DOT code."""
subgraphs = ''.join(
g.to_dot(
graph_type='subgraph')
for g in self.subgraphs)
def format_attributes(
attr
) -> str:
"""Return formatted assignment."""
return ', '.join(
f'{k}="{v}"'
for k, v in attr.items())
def format_node(
u,
attr
) -> str:
"""Return DOT code for node."""
attributes = format_attributes(attr)
return f'{u} [{attributes}];'
def format_edge(
u,
v,
attr
) -> str:
"""Return DOT code for edge."""
attributes = format_attributes(attr)
return f'{u} -> {v} [{attributes}];'
nodes = '\n'.join(
format_node(u, attr)
for u, attr in self.nodes.items())
edges = list()
for (u, v), attrs in self.edges.items():
for attr in attrs:
edge = format_edge(u, v, attr)
edges.append(edge)
edges = '\n'.join(edges)
indent_level = 4 * '\x20'
def fmt(
text:
str
) -> str:
"""Return indented text."""
newline = '\n' if text else ''
return newline + _tw.indent(
text,
prefix=4 * indent_level)
nodes = fmt(nodes)
edges = fmt(edges)
subgraphs = fmt(subgraphs)
if graph_type is None:
graph_type = self.graph_type
if self.rank is None:
rank = ''
else:
rank = f'rank = {self.rank}'
return _tw.dedent(f'''
{graph_type} {{
{rank}{nodes}{edges}{subgraphs}
}}
''')

def dump(
self,
filename:
str,
filetype:
str,
**kw
) -> None:
"""Write to file."""
if filetype not in DOT_FILE_TYPES:
raise ValueError(
f'Unknown file type "{filetype}" '
f'for "{filename}"')
dot_code = self.to_dot()
if filetype == 'dot':
with open(filename, 'w') as fd:
fd.write(dot_code)
return
dot = _sh.split(f'''
dot
-T{filetype}
-o '{filename}'
''')
_sbp.run(
dot,
encoding='utf8',
input=dot_code,
capture_output=True,
check=True)
8 changes: 5 additions & 3 deletions dd/autoref.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,12 +486,14 @@ def dump(
filetype = 'png'
elif name.endswith('.svg'):
filetype = 'svg'
elif name.endswith('.dot'):
filetype = 'dot'
elif name.endswith('.p'):
filetype = 'pickle'
elif name.endswith('.json'):
filetype = 'json'
else:
raise Exception(
raise ValueError(
'cannot infer file type '
'from extension of file '
f'name "{filename}"')
Expand All @@ -500,8 +502,8 @@ def dump(
raise ValueError(roots)
_copy.dump_json(roots, filename)
return
elif filetype not in (
'pickle', 'pdf', 'png', 'svg'):
elif (filetype != 'pickle' and
filetype not in _utils.DOT_FILE_TYPES):
raise ValueError(filetype)
if roots is not None:
def mapper(u):
Expand Down
Loading

0 comments on commit 12906ed

Please sign in to comment.