From 852ba3761f0bcd93ade3ad4be4bda1f325eb76f2 Mon Sep 17 00:00:00 2001 From: Oliver Lindemann Date: Sat, 15 Nov 2014 14:36:15 +0100 Subject: [PATCH 1/7] BUG: observed overrides all foreground colours --- daft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daft.py b/daft.py index dd27d07..100e0e0 100644 --- a/daft.py +++ b/daft.py @@ -280,7 +280,7 @@ def render(self, ctx): p["fc"] = fc # Draw the foreground ellipse. - if ctx.observed_style == "inner" and not self.fixed: + if ctx.observed_style == "inner" and not self.fixed and self.observed: p["fc"] = "none" el = Ellipse(xy=ctx.convert(self.x, self.y), width=diameter * aspect, height=diameter, **p) From 80f831ea61712f806daf31a39d0c32eb30cbf62b Mon Sep 17 00:00:00 2001 From: Oliver Lindemann Date: Sat, 15 Nov 2014 17:11:49 +0100 Subject: [PATCH 2/7] "update" --- .gitignore | 1 + daft.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 3311046..1cc3bdb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist *.egg-info *~ *.png +*.swp diff --git a/daft.py b/daft.py index 100e0e0..482aae0 100644 --- a/daft.py +++ b/daft.py @@ -174,13 +174,14 @@ class Node(object): """ def __init__(self, name, content, x, y, scale=1, aspect=None, - observed=False, fixed=False, + observed=False, fixed=False, rectangle=False, offset=[0, 0], plot_params={}, label_params=None): # Node style. assert not (observed and fixed), \ "A node cannot be both 'observed' and 'fixed'." self.observed = observed self.fixed = fixed + self.rectangle = rectangle # Metadata. self.name = name @@ -272,7 +273,12 @@ def render(self, ctx): p["fc"] = fc # Draw the background ellipse. - bg = Ellipse(xy=ctx.convert(self.x, self.y), + if self.rectangle: + bg = Rectangle(xy=ctx.convert(self.x-w/2, self.y-h/2), + width=w, height=h, **p) + print "hi" + else: + bg = Ellipse(xy=ctx.convert(self.x, self.y), width=w, height=h, **p) ax.add_artist(bg) @@ -282,7 +288,13 @@ def render(self, ctx): # Draw the foreground ellipse. if ctx.observed_style == "inner" and not self.fixed and self.observed: p["fc"] = "none" - el = Ellipse(xy=ctx.convert(self.x, self.y), + if self.rectangle: + xy = ctx.convert(self.x, self.y) + xy = (xy[0]-diameter/2.0, xy[1]-diameter/2.0) + el = Rectangle(xy=xy, + width=diameter * aspect, height=diameter, **p) + else: + el = Ellipse(xy=ctx.convert(self.x, self.y), width=diameter * aspect, height=diameter, **p) ax.add_artist(el) @@ -351,9 +363,12 @@ def _get_coords(self, ctx): dist1 = np.sqrt(dy * dy + dx * dx / float(a1 ** 2)) dist2 = np.sqrt(dy * dy + dx * dx / float(a2 ** 2)) + radius1 = 0.5 * ctx.node_unit * self.node1.scale + radius2 = 0.5 * ctx.node_unit * self.node2.scale + # Compute the fractional effect of the radii of the nodes. - alpha1 = 0.5 * ctx.node_unit * self.node1.scale / dist1 - alpha2 = 0.5 * ctx.node_unit * self.node2.scale / dist2 + alpha1 = radius1 / dist1 + alpha2 = radius2 / dist2 # Get the coordinates of the starting position. x0, y0 = x1 + alpha1 * dx, y1 + alpha1 * dy @@ -362,6 +377,18 @@ def _get_coords(self, ctx): dx0 = dx * (1. - alpha1 - alpha2) dy0 = dy * (1. - alpha1 - alpha2) + if self.node1.rectangle: + print self.node1.name + # replace start coordinates (polar) + _, angle = cart2polar(dx0, dy0) + angle = angle + np.pi/2.0 + displacment_dist = radius1/abs(np.cos(angle)) - radius1 + displacement = polar2cart(displacment_dist, angle) + print vec2deg(angle), np.cos(angle), displacment_dist + + x0 += displacement[1] + y0 -= displacement[0] + return x0, y0, dx0, dy0 def render(self, ctx): @@ -621,3 +648,18 @@ def _pop_multiple(d, default, *args): return default return results[0][1] + +def polar2cart(r, theta): + """polar coordinates to cartesian coordinates""" + x = r * np.cos(theta) + y = r * np.sin(theta) + return x, y + +def cart2polar(x,y): + """ cartesian coordinates to polar coordinates""" + r = np.sqrt(x**2 + y**2) + theta = np.arctan2(y, x) + return r, theta + +def vec2deg(vec): + return 180*vec/np.pi From 99e422c1b1f4977aa439d2b42a189b4cf0889a46 Mon Sep 17 00:00:00 2001 From: Oliver Lindemann Date: Sat, 15 Nov 2014 18:34:22 +0100 Subject: [PATCH 3/7] add rectangle --- daft.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/daft.py b/daft.py index 482aae0..68dfb2b 100644 --- a/daft.py +++ b/daft.py @@ -378,16 +378,22 @@ def _get_coords(self, ctx): dy0 = dy * (1. - alpha1 - alpha2) if self.node1.rectangle: - print self.node1.name # replace start coordinates (polar) - _, angle = cart2polar(dx0, dy0) - angle = angle + np.pi/2.0 - displacment_dist = radius1/abs(np.cos(angle)) - radius1 + length, angle = cart2polar(dx0, dy0) + if abs(angle)>np.pi*0.25 and abs(angle) Date: Sat, 15 Nov 2014 18:42:52 +0100 Subject: [PATCH 4/7] bug fix --- daft.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/daft.py b/daft.py index 68dfb2b..ad860ea 100644 --- a/daft.py +++ b/daft.py @@ -377,7 +377,7 @@ def _get_coords(self, ctx): dx0 = dx * (1. - alpha1 - alpha2) dy0 = dy * (1. - alpha1 - alpha2) - if self.node1.rectangle: + if self.node1.rectangle or self.node2.rectangle: # replace start coordinates (polar) length, angle = cart2polar(dx0, dy0) if abs(angle)>np.pi*0.25 and abs(angle) Date: Sun, 16 Nov 2014 14:10:14 +0100 Subject: [PATCH 5/7] bigfixes & add property double --- daft.py | 53 ++++++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/daft.py b/daft.py index ad860ea..942ed3c 100644 --- a/daft.py +++ b/daft.py @@ -155,8 +155,13 @@ class Node(object): :param aspect: (optional) The aspect ratio width/height for elliptical nodes; default 1. - :param observed: (optional) - Should this be a conditioned variable? + :param rectangle: (optional) + If `True` node has a rectangular shape. + + :param double: (optional) + Double lines. This must be ``"inner"`` or ``"outer"``. + Node is shown as double shapes with the second shape plotted inside + or outside of the standard one, respectively. :param fixed: (optional) Should this be a fixed (not permitted to vary) variable? @@ -175,6 +180,7 @@ class Node(object): """ def __init__(self, name, content, x, y, scale=1, aspect=None, observed=False, fixed=False, rectangle=False, + double = "", offset=[0, 0], plot_params={}, label_params=None): # Node style. assert not (observed and fixed), \ @@ -182,6 +188,7 @@ def __init__(self, name, content, x, y, scale=1, aspect=None, self.observed = observed self.fixed = fixed self.rectangle = rectangle + self.double = double # Metadata. self.name = name @@ -257,26 +264,26 @@ def render(self, ctx): aspect = ctx.aspect # Set up an observed node. Note the fc INSANITY. - if self.observed: + if self.observed or self.double =="inner" or self.double=="outer": # Update the plotting parameters depending on the style of # observed node. h = float(diameter) w = aspect * float(diameter) - if ctx.observed_style == "shaded": + if ctx.observed_style == "shaded" and self.observed: p["fc"] = "0.7" - elif ctx.observed_style == "outer": + elif ctx.observed_style == "outer" or self.double == "outer": h = diameter + 0.1 * diameter w = aspect * diameter + 0.1 * diameter - elif ctx.observed_style == "inner": + elif ctx.observed_style == "inner" or self.double == "inner": h = diameter - 0.1 * diameter w = aspect * diameter - 0.1 * diameter p["fc"] = fc # Draw the background ellipse. if self.rectangle: - bg = Rectangle(xy=ctx.convert(self.x-w/2, self.y-h/2), - width=w, height=h, **p) - print "hi" + xy = np.array(ctx.convert(self.x, self.y)) - diameter/2.0 + xy = xy + (diameter-w)/float(2) + bg = Rectangle(xy=xy, width=w, height=h, **p) else: bg = Ellipse(xy=ctx.convert(self.x, self.y), width=w, height=h, **p) @@ -286,13 +293,13 @@ def render(self, ctx): p["fc"] = fc # Draw the foreground ellipse. - if ctx.observed_style == "inner" and not self.fixed and self.observed: + if ctx.observed_style == "inner" and not self.fixed and \ + (self.observed or self.double == "inner"): p["fc"] = "none" if self.rectangle: - xy = ctx.convert(self.x, self.y) - xy = (xy[0]-diameter/2.0, xy[1]-diameter/2.0) - el = Rectangle(xy=xy, - width=diameter * aspect, height=diameter, **p) + xy = np.array(ctx.convert(self.x, self.y)) - diameter/2.0 + el = Rectangle(xy=xy, width=diameter * aspect, + height=diameter, **p) else: el = Ellipse(xy=ctx.convert(self.x, self.y), width=diameter * aspect, height=diameter, **p) @@ -378,23 +385,22 @@ def _get_coords(self, ctx): dy0 = dy * (1. - alpha1 - alpha2) if self.node1.rectangle or self.node2.rectangle: - # replace start coordinates (polar) + # calc displacement of the edge (in direction of the edge) length, angle = cart2polar(dx0, dy0) if abs(angle)>np.pi*0.25 and abs(angle) Date: Thu, 12 Feb 2015 09:04:47 +0100 Subject: [PATCH 6/7] double line space & universal add method --- README.rst | 11 +++++++---- daft.py | 41 ++++++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index 672f9f2..c7a67ff 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,13 @@ +This a fork of the daft project by Daniel Foreman-Mackey, David W. Hogg, and +others (https://github.com/dfm/daft ). + +`Oliver Lindemann `_ + .. image:: https://raw.github.com/davidwhogg/daft/master/images/logo.png **Daft** is a Python package that uses `matplotlib `_ to render pixel-perfect *probabilistic graphical models* for publication 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 `_ -************************************************************** +them in any formats that matplotlib supports. Get more information at: +`daft-pgm.org `_ diff --git a/daft.py b/daft.py index 942ed3c..1b02228 100644 --- a/daft.py +++ b/daft.py @@ -1,8 +1,14 @@ -__all__ = ["PGM", "Node", "Edge", "Plate"] +"""This a fork of the daft project by Daniel Foreman-Mackey, David W. Hogg, +and others (https://github.com/dfm/daft ). + +O. Lindemann (https://github.com/lindemann09/daft) +""" +__all__ = ["PGM", "Node", "Edge", "Plate"] -__version__ = "0.0.3" +__version__ = "0.1" +__author__ = "Oliver Lindemann" import matplotlib.pyplot as plt from matplotlib.patches import Ellipse @@ -131,6 +137,19 @@ def render(self): return self.ax + def add(self, plate_or_note): + """ + Add a :class: `Plate` or `Node` to the model. + + """ + if type(plate_or_note) == Node: + self.add_node(plate_or_note) + elif type(plate_or_note) == Plate: + self.add_plate(plate_or_note) + else: + raise RuntimeError("Known object to be added to model") + + class Node(object): """ @@ -177,11 +196,17 @@ class Node(object): A dictionary of parameters to pass to the :class:`matplotlib.patches.Ellipse` constructor. + :param space_double_line: (optional) + Distance between the two line for double lines notes. + default = 0.15 + """ + def __init__(self, name, content, x, y, scale=1, aspect=None, observed=False, fixed=False, rectangle=False, double = "", - offset=[0, 0], plot_params={}, label_params=None): + offset=[0, 0], plot_params={}, label_params=None, + space_double_line=0.15): # Node style. assert not (observed and fixed), \ "A node cannot be both 'observed' and 'fixed'." @@ -189,6 +214,7 @@ def __init__(self, name, content, x, y, scale=1, aspect=None, self.fixed = fixed self.rectangle = rectangle self.double = double + self.space_double_line = space_double_line # Metadata. self.name = name @@ -272,11 +298,11 @@ def render(self, ctx): if ctx.observed_style == "shaded" and self.observed: p["fc"] = "0.7" elif ctx.observed_style == "outer" or self.double == "outer": - h = diameter + 0.1 * diameter - w = aspect * diameter + 0.1 * diameter + h = diameter + self.space_double_line + w = aspect * diameter + self.space_double_line elif ctx.observed_style == "inner" or self.double == "inner": - h = diameter - 0.1 * diameter - w = aspect * diameter - 0.1 * diameter + h = diameter - self.space_double_line + w = aspect * diameter - self.space_double_line p["fc"] = fc # Draw the background ellipse. @@ -457,6 +483,7 @@ class Plate(object): :param rect: The rectangle describing the plate bounds in model coordinates. + [left, bottom, width, height] :param label: (optional) A string to annotate the plate. From 4f21c1f6eeeeaa86c3e44237cfeabdab345054be Mon Sep 17 00:00:00 2001 From: Oliver Lindemann Date: Thu, 12 Feb 2015 15:52:17 +0100 Subject: [PATCH 7/7] text annotations --- daft.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/daft.py b/daft.py index 1b02228..c4942be 100644 --- a/daft.py +++ b/daft.py @@ -1,5 +1,5 @@ -"""This a fork of the daft project by Daniel Foreman-Mackey, David W. Hogg, -and others (https://github.com/dfm/daft ). +"""This a fork of the daft project by Daniel Foreman-Mackey, David W. Hogg, +and others (https://github.com/dfm/daft ). O. Lindemann (https://github.com/lindemann09/daft) """ @@ -63,6 +63,7 @@ def __init__(self, shape, origin=[0, 0], self._nodes = {} self._edges = [] self._plates = [] + self._annotations = [] self._ctx = _rendering_context(shape=shape, origin=origin, grid_unit=grid_unit, @@ -116,6 +117,15 @@ def add_plate(self, plate): self._plates.append(plate) return None + def add_annotation(self, position, text, **kwargs): + """ + Add an annotation text to the model. + + """ + + self._annotations.append([position, text, kwargs]) + return + def render(self): """ Render the :class:`Plate`, :class:`Edge` and :class:`Node` objects in @@ -135,6 +145,10 @@ def render(self): for name, node in self._nodes.iteritems(): node.render(self._ctx) + for pos, text, kwargs in self._annotations: + self.ax.annotate(text, self._ctx.convert(pos[0], pos[1]), + **kwargs) + return self.ax def add(self, plate_or_note):