Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added interactive mode #91

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d0cb2d2
Added interactive mode
ixkael Oct 3, 2016
da954e2
Added interactive=True to all examples
ixkael Oct 5, 2016
309a971
Added python notebook reproducing the classic example
ixkael Oct 11, 2016
1bcec6f
Fixed path to include daft package, needed by mybinder
drphilmarshall Oct 11, 2016
07807b8
Added binder badge
drphilmarshall Oct 11, 2016
717b443
Links to binder
drphilmarshall Oct 11, 2016
04b2fb8
All examples now available in notebooks
drphilmarshall Oct 11, 2016
d5f87ce
Merge branch 'master' of github.com:drphilmarshall/daft
drphilmarshall Oct 11, 2016
fe2cc94
Link to binder status page
drphilmarshall Oct 11, 2016
68aa3ef
Added docker file
ixkael Oct 12, 2016
648bc4a
Merge pull request #1 from drphilmarshall/master
ixkael Oct 12, 2016
ad27e38
Merge pull request #2 from ixkael/pr/1
ixkael Oct 12, 2016
d23ed7a
trying default jupiter config
ixkael Oct 14, 2016
10dd355
Merge pull request #3 from ixkael/pr/1
ixkael Oct 14, 2016
3439048
Update Dockerfile
ixkael Oct 14, 2016
f9204c4
Delete Dockerfile
ixkael Oct 15, 2016
a37365c
Create Dockerfile
ixkael Oct 17, 2016
28c1115
Update Dockerfile
ixkael Oct 17, 2016
abb34db
Update Dockerfile
ixkael Oct 17, 2016
6f40931
Update Dockerfile
ixkael Oct 17, 2016
895985d
Update
ixkael Oct 17, 2016
d00cc6e
Update Dockerfile
ixkael Oct 17, 2016
0353cbc
Update Dockerfile
ixkael Oct 17, 2016
06e4ea7
Update Dockerfile
ixkael Oct 17, 2016
4fa4435
Update Dockerfile
ixkael Oct 17, 2016
9a80d49
Link to dfm's mybinder
ixkael Oct 20, 2016
ddc9d22
fixed issue with default matplotlib colors for annotations in plates
ixkael Nov 7, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ dist
*.egg-info
*~
*.png
*ipynb_checkpoints*
9 changes: 9 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM ubuntu:trusty

RUN apt-get update -q
RUN apt-get install -qy texlive-full
RUN apt-get install -qy python-pygments
RUN apt-get install -qy gnuplot

WORKDIR /data
VOLUME ["/data"]
12 changes: 11 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,15 @@ in a journal or on the internet. With a short Python script and an intuitive
model-building syntax you can design directed and undirected graphs and save
them in any formats that matplotlib supports.

Get more information at: `daft-pgm.org <http://daft-pgm.org>`_
Get more information at `daft-pgm.org <http://daft-pgm.org>`_

Try making some PGMs with the `example notebooks <http://mybinder.org:/repo/drphilmarshall/daft>`_

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ixkael If you know this is going to work, I guess we should change all these drphilmarshall URLs to dfm ones instead...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ixkael I still see drphilmarshall URLs... but you must be so close to that Kwak by now! :-)


.. image:: http://mybinder.org/badge.svg
:target: http://mybinder.org:/repo/drphilmarshall/daft


(You may need to `rebuild the binder <http://mybinder.org/status/drphilmarshall/daft>`_.)

**************************************************************

198 changes: 180 additions & 18 deletions daft.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@
from matplotlib.patches import Ellipse
from matplotlib.patches import FancyArrow
from matplotlib.patches import Rectangle as Rectangle
from matplotlib.text import Annotation

import numpy as np

class Tree(object):
"""
Simple generic tree implementation for storing
and connecting artists when rendering the PGM.
"""
def __init__(self, data=None, branches=None):
self.root = data
if branches is None:
self.branches = []
else:
self.branches = branches

def add_branch(self, obj):
self.branches.append(obj)

class PGM(object):
"""
Expand Down Expand Up @@ -111,7 +126,22 @@ def add_plate(self, plate):
self._plates.append(plate)
return None

def render(self):
def __str__(self):
"""
Print the positions of :class:`Plate` and :class:`Node` objects in
the model. This is useful if you interactively edited the model
and want to know the new parameters for later reuse.
"""
st = ""
for name in self._nodes:
st += self._nodes[name].__str__() + "\n"

for plate in self._plates:
st += plate.__str__() + "\n"

return st

def render(self, interactive=False):
"""
Render the :class:`Plate`, :class:`Edge` and :class:`Node` objects in
the model. This will create a new figure with the correct dimensions
Expand All @@ -120,18 +150,95 @@ def render(self):
"""
self.figure = self._ctx.figure()
self.ax = self._ctx.ax()
self.artistTreeList = {}
# Artist tree will contain a dictionary of Nodes and Plates
# with pointers to their artists (lines, ellipses, text, arrows)

for name in self._nodes:
artistTree = self._nodes[name].render(self._ctx)
self.artistTreeList.update({self._nodes[name]: artistTree})

for plate in self._plates:
plate.render(self._ctx)
artistTree = plate.render(self._ctx)
self.artistTreeList.update({plate: artistTree})

for edge in self._edges:
edge.render(self._ctx)

for name in self._nodes:
self._nodes[name].render(self._ctx)
# Add each arrow to the node1 and node2 trees.
self.artistTreeList[edge.node1].add_branch(edge)
self.artistTreeList[edge.node2].add_branch(edge)

if interactive:
# Collect artists
self.artists = [key.root for key in self.artistTreeList.values()]
tolerance = 5 # some tolerance for grabbing artists
for artist in self.artists:
artist.set_picker(tolerance)
self.currently_dragging = False
self.current_artist = None
self.offset = (0, 0)
for canvas in set(artist.figure.canvas for artist in self.artists):
canvas.mpl_connect('button_press_event', self.on_press)
canvas.mpl_connect('button_release_event', self.on_release)
canvas.mpl_connect('pick_event', self.on_pick)
canvas.mpl_connect('motion_notify_event', self.on_motion)
plt.show()

return self.ax

def on_press(self, event):
"""Event: click"""
self.currently_dragging = True

def on_release(self, event):
"""Event: releasing artists"""
self.currently_dragging = False
self.current_artist = None

def on_pick(self, event):
"""
Picking artists
"""
if self.current_artist is None:
self.current_artist = event.artist
x1, y1 = event.mouseevent.xdata, event.mouseevent.ydata
# Offset of artist position with respect to click
if isinstance(self.current_artist, Rectangle):
x0, y0 = self.current_artist.xy
elif isinstance(self.current_artist, Ellipse):
x0, y0 = self.current_artist.center
else:
x0, y0 = x1, y1
self.offset = (x0 - x1), (y0 - y1)

def on_motion(self, event):
"""
Moving artist and changing the content of the relevant Node/Plate
"""
if not self.currently_dragging:
return
if self.current_artist is None:
return
dx, dy = self.offset # offset of artist center w.r. to click
xp, yp = event.xdata + dx, event.ydata + dy # plot space
xm, ym = self._ctx.invconvert(xp, yp) # model space
for k, v in self.artistTreeList.items():
if v.root == self.current_artist:
k.move(xm, ym, xp, yp)
for key, tree in self.artistTreeList.items():
# Traverse list to get the right Plate/Node and its artistTree
if tree.root == self.current_artist:
# Move the dependent artists
for ar in tree.branches:
if isinstance(ar, Edge):
ar.ar.remove() # removing artist
ar.render(self._ctx) # drawing it again
if isinstance(ar, (Rectangle, Annotation)):
ar.xy = xp, yp
if isinstance(ar, Ellipse):
ar.center = xp, yp
self.current_artist.figure.canvas.draw()


class Node(object):
"""
Expand Down Expand Up @@ -190,8 +297,9 @@ def __init__(self, name, content, x, y, scale=1, aspect=None,
# Coordinates and dimensions.
self.x, self.y = x, y
self.scale = scale
self.scalefac = 6.0
if self.fixed:
self.scale /= 6.0
self.scale /= self.scalefac
self.aspect = aspect

# Display parameters.
Expand All @@ -204,6 +312,26 @@ def __init__(self, name, content, x, y, scale=1, aspect=None,
else:
self.label_params = None

def __str__(self):
"""
Print the input parameters of
"""
st = "Node("
st += "'" + str(self.name) + "'"
st += ", " + "r'" + str(self.content) + "'"
st += ", " + str(self.x)
if self.fixed:
st += ", scale=" + str(self.scalefac * self.scale)
else:
st += ", scale=" + str(self.scale)
st += ", " + str(self.y)
for atnm in ['aspect', 'observed', 'fixed', 'offset', 'plot_params', 'label_params']:
at = getattr(self, atnm)
if at is not None:
st += ", " + atnm + "=" + str(at)
st += ")"
return st

def render(self, ctx):
"""
Render the node.
Expand Down Expand Up @@ -256,6 +384,7 @@ def render(self, ctx):
else:
aspect = ctx.aspect

self.artistTree = Tree()
# Set up an observed node. Note the fc INSANITY.
if self.observed:
# Update the plotting parameters depending on the style of
Expand All @@ -276,27 +405,33 @@ def render(self, ctx):
bg = Ellipse(xy=ctx.convert(self.x, self.y),
width=w, height=h, **p)
ax.add_artist(bg)
self.artistTree.add_branch(bg)

# Reset the face color.
p["fc"] = fc

# Draw the foreground ellipse.
if ctx.observed_style == "inner" and not self.fixed:
p["fc"] = "none"
el = Ellipse(xy=ctx.convert(self.x, self.y),
self.artistTree.root = Ellipse(xy=ctx.convert(self.x, self.y),
width=diameter * aspect, height=diameter, **p)
ax.add_artist(el)
ax.add_artist(self.artistTree.root)

# Reset the face color.
p["fc"] = fc

# Annotate the node.
ax.annotate(self.content, ctx.convert(self.x, self.y),
an = ax.annotate(self.content, ctx.convert(self.x, self.y),
xycoords="data",
xytext=self.offset, textcoords="offset points",
**l)
self.artistTree.add_branch(an)

return self.artistTree

return el
def move(self, xm, ym, xp, yp):
self.x, self.y = xm, ym
self.artistTree.root.center = xp, yp


class Edge(object):
Expand Down Expand Up @@ -382,7 +517,7 @@ def render(self, ctx):
# Add edge annotation.
if "label" in self.plot_params:
x, y, dx, dy = self._get_coords(ctx)
ax.annotate(self.plot_params["label"],
an = ax.annotate(self.plot_params["label"],
[x + 0.5 * dx, y + 0.5 * dy], xycoords="data",
xytext=[0, 3], textcoords="offset points",
ha="center", va="center")
Expand All @@ -394,22 +529,21 @@ def render(self, ctx):
p["head_width"] = p.get("head_width", 0.1)

# Build an arrow.
ar = FancyArrow(*self._get_coords(ctx), width=0,
self.ar = FancyArrow(*self._get_coords(ctx), width=0,
length_includes_head=True,
**p)

# Add the arrow to the axes.
ax.add_artist(ar)
return ar
ax.add_artist(self.ar)
else:
p["color"] = p.get("color", "k")

# Get the right coordinates.
x, y, dx, dy = self._get_coords(ctx)

# Plot the line.
line = ax.plot([x, x + dx], [y, y + dy], **p)
return line
self.ar = ax.plot([x, x + dx], [y, y + dy], **p)
return self.ar


class Plate(object):
Expand Down Expand Up @@ -447,6 +581,21 @@ def __init__(self, rect, label=None, label_offset=[5, 5], shift=0,
self.bbox = dict(bbox)
self.position = position

def __str__(self):
"""
Print the input parameters of
"""
st = "Plate("
st += str(self.rect)
st += ", label=r'" + self.label + "'"
st += ", position='" + self.position + "'"
for atnm in ['label_offset', 'shift', 'rect_params', 'bbox']:
at = getattr(self, atnm)
if at is not None:
st += ", " + atnm + "=" + str(at)
st += ")"
return st

def render(self, ctx):
"""
Render the plate in the given axes.
Expand Down Expand Up @@ -485,13 +634,18 @@ def render(self, ctx):
raise RuntimeError("Unknown positioning string: {0}"
.format(self.position))

ax.annotate(self.label, pos, xycoords="data",
an = ax.annotate(self.label, pos, xycoords="data",
xytext=offset, textcoords="offset points",
bbox=self.bbox,
horizontalalignment=ha)

return rect
self.artistTree = Tree(rect, [an])

return self.artistTree

def move(self, xm, ym, xp, yp):
self.x, self.y = xm, ym
self.artistTree.root.xy = xp, yp

class _rendering_context(object):
"""
Expand Down Expand Up @@ -585,6 +739,14 @@ def convert(self, *xy):
assert len(xy) == 2
return self.grid_unit * (np.atleast_1d(xy) - self.origin)

def invconvert(self, *xy):
"""
Convert from plot coordinates to model coordinates.

"""
assert len(xy) == 2
return self.origin + np.atleast_1d(xy) / self.grid_unit


def _pop_multiple(d, default, *args):
"""
Expand Down
2 changes: 1 addition & 1 deletion examples/astronomy.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,6 @@
pgm.add_edge("cosmic rays", "noise patch")

# Render and save.
pgm.render()
pgm.render(interactive=True)
pgm.figure.savefig("astronomy.pdf")
pgm.figure.savefig("astronomy.png", dpi=150)
2 changes: 1 addition & 1 deletion examples/badfont.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
pgm.add_edge("confused", "ugly")
pgm.add_edge("ugly", "bad")
pgm.add_edge("confused", "bad")
pgm.render()
pgm.render(interactive=True)
pgm.figure.savefig("badfont.pdf")
pgm.figure.savefig("badfont.png", dpi=150)
2 changes: 1 addition & 1 deletion examples/bca.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
pgm.add_plate(daft.Plate([0.5, 2.25, 1, 1.25], label=r"data $n$"))
pgm.add_edge("a", "b")
pgm.add_edge("b", "c")
pgm.render()
pgm.render(interactive=True)
pgm.figure.savefig("bca.pdf")
pgm.figure.savefig("bca.png", dpi=150)
Loading