Skip to content

Commit 17ffd1a

Browse files
Merge pull request #212 from egraphs-good/interactive-visualizer
Make visualizations interactive
2 parents b7c6762 + ae215ba commit 17ffd1a

File tree

16 files changed

+35944
-211
lines changed

16 files changed

+35944
-211
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,5 @@ Source.*
8383
3
8484
4
8585
inlined
86+
visualizer.tgz
87+
package

Makefile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
2+
all: python/egglog/visualizer.js
3+
4+
5+
6+
# download visualizer release from github
7+
#
8+
visualizer.tgz:
9+
curl -s https://api.github.com/repos/egraphs-good/egraph-visualizer/releases/latest \
10+
| grep "browser_download_url.*tgz" \
11+
| cut -d : -f 2,3 \
12+
| tr -d \" \
13+
| wget -qi - -O visualizer.tgz
14+
15+
# extract visualizer release
16+
python/egglog/visualizer.js python/egglog/visualizer.css: visualizer.tgz
17+
tar -xzf visualizer.tgz
18+
rm visualizer.tgz
19+
mv package/dist/index.js python/egglog/visualizer.js
20+
mv package/dist/style.css python/egglog/visualizer.css
21+
rm -rf package
22+
23+
clean:
24+
rm -rf package python/egglog/visualizer.css python/egglog/visualizer.js visualizer.tgz

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ _This project uses semantic versioning_
1010
- Adds source annotations to expressions for tracebacks
1111
- Adds ability to inline other functions besides primitives in serialized output
1212
- Adds `remove` and `set` methods to `Vec`
13+
- Upgrades to use the new egraph-visualizer so we can have interactive visualizations
1314

1415
## 7.2.0 (2024-05-23)
1516

docs/index.md

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
file_format: mystnb
33
---
44

5-
# [`egglog`](https://github.com/egraphs-good/egglog/) Python
5+
# `egglog` Python
66

7-
`egglog` is a Python package that provides bindings to the Rust library of the same name,
7+
`egglog` is a Python package that provides bindings to [the Rust library of the same name](https://github.com/egraphs-good/egglog/),
88
allowing you to use e-graphs in Python for optimization, symbolic computation, and analysis.
99

1010
It wraps the Rust library [`egglog`](https://github.com/egraphs-good/egglog) which
@@ -13,14 +13,10 @@ See the ["Better Together: Unifying Datalog and Equality Saturation"](https://ar
1313

1414
> We present egglog, a fixpoint reasoning system that unifies Datalog and equality saturation (EqSat). Like Datalog, it supports efficient incremental execution, cooperating analyses, and lattice-based reasoning. Like EqSat, it supports term rewriting, efficient congruence closure, and extraction of optimized terms.
1515
16-
## [Installation](./reference/usage)
17-
1816
```shell
1917
pip install egglog
2018
```
2119

22-
## Example
23-
2420
```{code-cell} python
2521
from __future__ import annotations
2622
from egglog import *
@@ -49,15 +45,10 @@ def _num_rule(a: Num, b: Num, c: Num, i: i64, j: i64):
4945
yield rewrite(Num(i) * Num(j)).to(Num(i * j))
5046
5147
egraph.saturate()
52-
```
53-
54-
```{code-cell} python
5548
egraph.check(eq(expr1).to(expr2))
5649
egraph.extract(expr1)
5750
```
5851

59-
## Contents
60-
6152
```{toctree}
6253
:maxdepth: 2
6354
tutorials

docs/reference/contributing.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,14 @@ conda activate egglog-python
3232
Then install the package in editable mode with the development dependencies:
3333

3434
```bash
35-
maturin develop -E .[dev]
35+
maturin develop -E dev,docs,test,array
3636
```
3737

3838
Anytime you change the rust code, you can run `maturin develop -E` to recompile the rust code.
3939

40+
If you would like to download a new version of the visualizer source, run `make clean; make`. This will download
41+
the most recent released version from the github actions artifact in the [egraph-visualizer](https://github.com/egraphs-good/egraph-visualizer) repo. It is checked in because it's a pain to get cargo to include only one git ignored file while ignoring the rest of the files that were ignored.
42+
4043
### Running Tests
4144

4245
To run the tests, you can use the `pytest` command:

docs/reference/python-integration.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,3 +537,30 @@ egraph.run(other_math_ruleset * 2)
537537
egraph.check(eq(x).to(WrappedMath(math_float(3.14)) + WrappedMath(math_float(3.14))))
538538
egraph
539539
```
540+
541+
## Visualization
542+
543+
The default renderer for the e-graph in a Jupyter Notebook [an interactive Javascript visualizer](https://github.com/egraphs-good/egraph-visualizer):
544+
545+
```{code-cell} python
546+
egraph
547+
```
548+
549+
You can also customize the visualization through using the <inv:egglog.EGraph.display> method:
550+
551+
```{code-cell} python
552+
egraph.display()
553+
```
554+
555+
If you would like to visualize the progression of the e-graph over time, you can use the <inv:egglog.EGraph.saturate> method to
556+
run a number of iterations and then visualize the e-graph at each step:
557+
558+
```{code-cell} python
559+
egraph = EGraph()
560+
egraph.register(Math(2) + Math(100))
561+
i, j = vars_("i j", i64)
562+
r = ruleset(
563+
rewrite(Math(i) + Math(j)).to(Math(i + j)),
564+
)
565+
egraph.saturate(r)
566+
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ classifiers = [
2525
"Topic :: Software Development :: Interpreters",
2626
"Typing :: Typed",
2727
]
28-
dependencies = ["typing-extensions", "black", "graphviz"]
28+
dependencies = ["typing-extensions", "black", "graphviz", "anywidget"]
2929

3030
[project.optional-dependencies]
3131

python/egglog/conversion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def resolve_literal(
180180
# Try all parent types as well, if we are converting from a Python type
181181
for arg_type_instance in arg_type.__mro__ if isinstance(arg_type, type) else [arg_type]:
182182
try:
183-
fn = CONVERSIONS[(cast(TypeName | type, arg_type_instance), tp_name)][1]
183+
fn = CONVERSIONS[(arg_type_instance, tp_name)][1]
184184
except KeyError:
185185
continue
186186
break

python/egglog/egraph.py

Lines changed: 88 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,6 @@
3838
from .thunk import *
3939

4040
if TYPE_CHECKING:
41-
import ipywidgets
42-
4341
from .builtins import Bool, PyObject, String, f64, i64
4442

4543

@@ -973,6 +971,7 @@ class GraphvizKwargs(TypedDict, total=False):
973971
n_inline_leaves: int
974972
split_primitive_outputs: bool
975973
split_functions: list[object]
974+
include_temporary_functions: bool
976975

977976

978977
@dataclass
@@ -1015,82 +1014,8 @@ def as_egglog_string(self) -> str:
10151014
raise ValueError(msg)
10161015
return cmds
10171016

1018-
def _repr_mimebundle_(self, *args, **kwargs):
1019-
"""
1020-
Returns the graphviz representation of the e-graph.
1021-
"""
1022-
return {"image/svg+xml": self.graphviz().pipe(format="svg", quiet=True, encoding="utf-8")}
1023-
1024-
def graphviz(self, **kwargs: Unpack[GraphvizKwargs]) -> graphviz.Source:
1025-
# By default we want to split primitive outputs
1026-
split_primitive_outputs = kwargs.pop("split_primitive_outputs", True)
1027-
split_additional_functions = kwargs.pop("split_functions", [])
1028-
n_inline = kwargs.pop("n_inline_leaves", 0)
1029-
serialized = self._egraph.serialize(
1030-
[],
1031-
max_functions=kwargs.pop("max_functions", None),
1032-
max_calls_per_function=kwargs.pop("max_calls_per_function", None),
1033-
include_temporary_functions=False,
1034-
)
1035-
if split_primitive_outputs or split_additional_functions:
1036-
additional_ops = set(map(self._callable_to_egg, split_additional_functions))
1037-
serialized.split_e_classes(self._egraph, additional_ops)
1038-
serialized.map_ops(self._state.op_mapping())
1039-
1040-
for _ in range(n_inline):
1041-
serialized.inline_leaves()
1042-
original = serialized.to_dot()
1043-
# Add link to stylesheet to the graph, so that edges light up on hover
1044-
# https://gist.github.com/sverweij/93e324f67310f66a8f5da5c2abe94682
1045-
styles = """/* the lines within the edges */
1046-
.edge:active path,
1047-
.edge:hover path {
1048-
stroke: fuchsia;
1049-
stroke-width: 3;
1050-
stroke-opacity: 1;
1051-
}
1052-
/* arrows are typically drawn with a polygon */
1053-
.edge:active polygon,
1054-
.edge:hover polygon {
1055-
stroke: fuchsia;
1056-
stroke-width: 3;
1057-
fill: fuchsia;
1058-
stroke-opacity: 1;
1059-
fill-opacity: 1;
1060-
}
1061-
/* If you happen to have text and want to color that as well... */
1062-
.edge:active text,
1063-
.edge:hover text {
1064-
fill: fuchsia;
1065-
}"""
1066-
p = pathlib.Path(tempfile.gettempdir()) / "graphviz-styles.css"
1067-
p.write_text(styles)
1068-
with_stylesheet = original.replace("{", f'{{stylesheet="{p!s}"', 1)
1069-
return graphviz.Source(with_stylesheet)
1070-
1071-
def graphviz_svg(self, **kwargs: Unpack[GraphvizKwargs]) -> str:
1072-
return self.graphviz(**kwargs).pipe(format="svg", quiet=True, encoding="utf-8")
1073-
1074-
def _repr_html_(self) -> str:
1075-
"""
1076-
Add a _repr_html_ to be an SVG to work with sphinx gallery.
1077-
1078-
ala https://github.com/xflr6/graphviz/pull/121
1079-
until this PR is merged and released
1080-
https://github.com/sphinx-gallery/sphinx-gallery/pull/1138
1081-
"""
1082-
return self.graphviz_svg()
1083-
1084-
def display(self, **kwargs: Unpack[GraphvizKwargs]) -> None:
1085-
"""
1086-
Displays the e-graph in the notebook.
1087-
"""
1088-
if IN_IPYTHON:
1089-
from IPython.display import SVG, display
1090-
1091-
display(SVG(self.graphviz_svg(**kwargs)))
1092-
else:
1093-
self.graphviz(**kwargs).render(view=True, format="svg", quiet=True)
1017+
def _ipython_display_(self) -> None:
1018+
self.display()
10941019

10951020
def input(self, fn: Callable[..., String], path: str) -> None:
10961021
"""
@@ -1319,40 +1244,100 @@ def eval(self, expr: Expr) -> object:
13191244
return self._egraph.eval_py_object(egg_expr)
13201245
raise TypeError(f"Eval not implemented for {typed_expr.tp}")
13211246

1322-
def saturate(
1247+
def _serialize(
13231248
self,
1324-
schedule: Schedule | None = None,
1325-
*,
1326-
max: int = 1000,
1327-
performance: bool = False,
13281249
**kwargs: Unpack[GraphvizKwargs],
1329-
) -> ipywidgets.Widget:
1330-
from .graphviz_widget import graphviz_widget_with_slider
1250+
) -> bindings.SerializedEGraph:
1251+
max_functions = kwargs.pop("max_functions", None)
1252+
max_calls_per_function = kwargs.pop("max_calls_per_function", None)
1253+
split_primitive_outputs = kwargs.pop("split_primitive_outputs", True)
1254+
split_functions = kwargs.pop("split_functions", [])
1255+
include_temporary_functions = kwargs.pop("include_temporary_functions", False)
1256+
n_inline_leaves = kwargs.pop("n_inline_leaves", 1)
1257+
serialized = self._egraph.serialize(
1258+
[],
1259+
max_functions=max_functions,
1260+
max_calls_per_function=max_calls_per_function,
1261+
include_temporary_functions=include_temporary_functions,
1262+
)
1263+
if split_primitive_outputs or split_functions:
1264+
additional_ops = set(map(self._callable_to_egg, split_functions))
1265+
serialized.split_e_classes(self._egraph, additional_ops)
1266+
serialized.map_ops(self._state.op_mapping())
13311267

1332-
dots = [str(self.graphviz(**kwargs))]
1333-
i = 0
1334-
while self.run(schedule or 1).updated and i < max:
1335-
i += 1
1336-
dots.append(str(self.graphviz(**kwargs)))
1337-
return graphviz_widget_with_slider(dots, performance=performance)
1268+
for _ in range(n_inline_leaves):
1269+
serialized.inline_leaves()
13381270

1339-
def saturate_to_html(
1340-
self, file: str = "tmp.html", performance: bool = False, **kwargs: Unpack[GraphvizKwargs]
1341-
) -> None:
1342-
# raise NotImplementedError("Upstream bugs prevent rendering to HTML")
1271+
return serialized
13431272

1344-
# import panel
1273+
def _graphviz(self, **kwargs: Unpack[GraphvizKwargs]) -> graphviz.Source:
1274+
serialized = self._serialize(**kwargs)
13451275

1346-
# panel.extension("ipywidgets")
1276+
original = serialized.to_dot()
1277+
# Add link to stylesheet to the graph, so that edges light up on hover
1278+
# https://gist.github.com/sverweij/93e324f67310f66a8f5da5c2abe94682
1279+
styles = """/* the lines within the edges */
1280+
.edge:active path,
1281+
.edge:hover path {
1282+
stroke: fuchsia;
1283+
stroke-width: 3;
1284+
stroke-opacity: 1;
1285+
}
1286+
/* arrows are typically drawn with a polygon */
1287+
.edge:active polygon,
1288+
.edge:hover polygon {
1289+
stroke: fuchsia;
1290+
stroke-width: 3;
1291+
fill: fuchsia;
1292+
stroke-opacity: 1;
1293+
fill-opacity: 1;
1294+
}
1295+
/* If you happen to have text and want to color that as well... */
1296+
.edge:active text,
1297+
.edge:hover text {
1298+
fill: fuchsia;
1299+
}"""
1300+
p = pathlib.Path(tempfile.gettempdir()) / "graphviz-styles.css"
1301+
p.write_text(styles)
1302+
with_stylesheet = original.replace("{", f'{{stylesheet="{p!s}"', 1)
1303+
return graphviz.Source(with_stylesheet)
13471304

1348-
widget = self.saturate(performance=performance, **kwargs)
1349-
# panel.panel(widget).save(file)
1305+
def display(self, graphviz: bool = False, **kwargs: Unpack[GraphvizKwargs]) -> None:
1306+
"""
1307+
Displays the e-graph.
1308+
1309+
If in IPython it will display it inline, otherwise it will write it to a file and open it.
1310+
"""
1311+
from IPython.display import SVG, display
1312+
1313+
from .visualizer_widget import VisualizerWidget
1314+
1315+
if graphviz:
1316+
if IN_IPYTHON:
1317+
svg = self._graphviz(**kwargs).pipe(format="svg", quiet=True, encoding="utf-8")
1318+
display(SVG(svg))
1319+
else:
1320+
self._graphviz(**kwargs).render(view=True, format="svg", quiet=True)
1321+
else:
1322+
serialized = self._serialize(**kwargs)
1323+
VisualizerWidget(egraphs=[serialized.to_json()]).display_or_open()
13501324

1351-
from ipywidgets.embed import embed_minimal_html
1325+
def saturate(self, schedule: Schedule | None = None, *, max: int = 1000, **kwargs: Unpack[GraphvizKwargs]) -> None:
1326+
"""
1327+
Saturate the egraph, running the given schedule until the egraph is saturated.
1328+
It serializes the egraph at each step and returns a widget to visualize the egraph.
1329+
"""
1330+
from .visualizer_widget import VisualizerWidget
1331+
1332+
def to_json() -> str:
1333+
return self._serialize(**kwargs).to_json()
13521334

1353-
embed_minimal_html("tmp.html", views=[widget], drop_defaults=False)
1354-
# Use panel while this issue persists
1355-
# https://github.com/jupyter-widgets/ipywidgets/issues/3761#issuecomment-1755563436
1335+
egraphs = [to_json()]
1336+
i = 0
1337+
while self.run(schedule or 1).updated and i < max:
1338+
i += 1
1339+
egraphs.append(to_json())
1340+
VisualizerWidget(egraphs=egraphs).display_or_open()
13561341

13571342
@classmethod
13581343
def current(cls) -> EGraph:

python/egglog/exp/array_api_jit.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ def jit(fn: X) -> X:
1414
"""
1515
Jit compiles a function
1616
"""
17-
from IPython.display import SVG
18-
1917
# 1. Create variables for each of the two args in the functions
2018
sig = inspect.signature(fn)
2119
arg1, arg2 = sig.parameters.keys()
@@ -25,13 +23,12 @@ def jit(fn: X) -> X:
2523
egraph.register(res)
2624
egraph.run(array_api_numba_schedule)
2725
res_optimized = egraph.extract(res)
28-
svg = SVG(egraph.graphviz_svg(split_primitive_outputs=True, n_inline_leaves=3))
26+
egraph.display(split_primitive_outputs=True, n_inline_leaves=3)
2927

3028
egraph = EGraph()
3129
fn_program = ndarray_function_two(res_optimized, NDArray.var(arg1), NDArray.var(arg2))
3230
egraph.register(fn_program)
3331
egraph.run(array_api_program_gen_schedule)
3432
fn = cast(X, egraph.eval(fn_program.py_object))
35-
fn.egraph = svg # type: ignore[attr-defined]
3633
fn.expr = res_optimized # type: ignore[attr-defined]
3734
return fn

0 commit comments

Comments
 (0)