From 25aecb2f2e3c79bf3c9bad9faef6bdd8ebd07f62 Mon Sep 17 00:00:00 2001 From: Robert Lachlan Date: Mon, 11 Feb 2013 15:47:42 -0800 Subject: [PATCH 01/11] Added max flow computing algorithm (edmonds-karp), supporting functions, and a few tests. --- project.clj | 2 +- src/loom/alg.clj | 22 +++++++- src/loom/flow.clj | 116 ++++++++++++++++++++++++++++++++++++++++ test/loom/test/flow.clj | 55 +++++++++++++++++++ 4 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/loom/flow.clj create mode 100644 test/loom/test/flow.clj diff --git a/project.clj b/project.clj index 9c71e40..c18e47f 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ (defproject jkkramer/loom "0.2.0" :description "Graph library for Clojure" :author "Justin Kramer" - :dependencies [[org.clojure/clojure "1.3.0"]]) + :dependencies [[org.clojure/clojure "1.4.0"]]) diff --git a/src/loom/alg.clj b/src/loom/alg.clj index c595bf4..82dc8d8 100644 --- a/src/loom/alg.clj +++ b/src/loom/alg.clj @@ -3,7 +3,8 @@ Graph, Digraph, or WeightedGraph protocols (as appropriate per algorithm) can use these functions." :author "Justin Kramer"} loom.alg - (:require [loom.alg-generic :as gen]) + (:require [loom.alg-generic :as gen] + [loom.flow :as flow]) (:use [loom.graph :only [add-edges nodes edges neighbors weight incoming degree in-degree weighted? directed? graph transpose] @@ -324,4 +325,23 @@ can use these functions." [#{} #{}] coloring))) +(defn max-flow + "Returns [flow-map flow-value], where flow-map is a weighted adjacency map + representing the maximum flow. The argument should be a weighted digraph, + where the edge weights are flow capacities. Source and sink are the vertices + representing the flow source and sink vertices. Optionally, pass in + :method :algorithm to use. Currently, the only option is :edmonds-karp ." + [g source sink & {:keys [method] :or {method :edmonds-karp}}] + (let [method-set #{:edmonds-karp} + n (nb g), i (incoming g), c (wt g), s source, t sink + [flow-map flow-value] (case method + :edmonds-karp (flow/edmonds-karp n i c s t) + (throw + (java.lang.RuntimeException. + (str "Method not found. Choose from: " + method-set))))] + [flow-map flow-value])) + + + ;; TODO: MST, coloring, matching, etc etc \ No newline at end of file diff --git a/src/loom/flow.clj b/src/loom/flow.clj new file mode 100644 index 0000000..821d20e --- /dev/null +++ b/src/loom/flow.clj @@ -0,0 +1,116 @@ +(ns ^{:doc "Algorithms for solving network flow" + :author "Robert Lachlan"} + loom.flow + (:require [loom.alg-generic :as gen :only [bf-path]])) + + +(defn residual-capacity [capacity flow v1 v2] + "Computes the residual capacity between nodes v1 and v2. Capacity is a function + that takes two nodes, and returns the capacity on the edge between them, if + any. Flow is the adjacency map which represents the current flow in the + network." + (+ + (or (get-in flow [v2 v1]) 0) + (- (or (capacity v1 v2) 0) + (or (get-in flow [v1 v2]) 0)))) + +(defn flow-balance [flow] + "Given a flow, returns a map of {node (sum(in weight) - sum(out weight))}" + (loop [out {}, in {}, adj-list (seq flow)] + (if-let [[node neighbours] (first adj-list)] + (recur (assoc out node (- (reduce + (vals neighbours)))) + (merge-with + in neighbours) + (next adj-list)) + (merge-with + out in)))) + +(defn satisfies-mass-balance? [flow source sink] + "Given a flow, verifies whether at each node the sum of in edge weights is equal + to the sum of out edge weights, except at the source and sink. The source + should have positive net outflow, the sink negative, and together they + should balance." + (let [balance (flow-balance flow)] + (and (<= (or (get balance source) 0) 0) + (zero? (+ (or (get balance source) 0) + (or (get balance sink) 0))) + (every? zero? (vals (dissoc balance source sink)))))) + +(defn satisfies-capacity-constraints? [flow capacity] + "Given a flow map, and a capacity function, verifies that the flow on each edge + is <= capacity of that edge." + (empty? + (remove (fn [[node flow-to-neighbors]] + (every? + (fn [[neighbor flow-value]] (<= flow-value (capacity node neighbor))) + (seq flow-to-neighbors))) + (seq flow)))) + +(defn is-admissible-flow? [flow capacity source sink] + "Verifies that a flow satisfies capacity and mass balance constraints. Does + verify that a flow is maximum." + (and (satisfies-mass-balance? flow source sink) + (satisfies-capacity-constraints? flow capacity))) + +(defn min-weight-along-path [path weight-fn] + "Given a path, represented by a sequence of nodes, and weight-function, computes + the minimum of the edge weights along the path. If an edge on the path is + missing, returns 0." + (reduce min (map #(or (apply weight-fn %) 0) (partition 2 1 path)))) + +(defn bf-find-augmenting-path [neighbors incoming capacity flow s t] + "Finds a shortest path in the flow network along which there remains residual + capacity. Neighbours is a function which, given a vertex, returns the + vertices connected by outgoing edges. Incoming, similarly is a function to get + vertices connected by incoming edges. Capacity is a function which takes two + vertices and returns the capacity between them. Flow is an adjacency map which + contains the current value of network flow. s is the source node, t the sink." + (gen/bf-path + (fn [vertex] (distinct (filter #(> (residual-capacity capacity flow vertex %) 0) + (concat (neighbors vertex) (incoming vertex))))) + s t)) + +(defn augment-along-path [flow capacity path increase] + "Given a flow represented as an adjacency map, returns an updated flow. + Capacity is a function of two vertices, path is a sequence of nodes, and + increase is the amount by which the flow should be augmented on this path. An + exception is thrown if the augmentation is impossible given capacity + constraints." + (let [vn0 (first path) + vn1 (second path) + forward-flow (or (get-in flow [vn0 vn1]) 0) + forward-capacity (- (or (capacity vn0 vn1) 0) forward-flow) + reverse-flow (or (get-in flow [vn1 vn0]) 0) + forward-increase (min forward-capacity increase) + pushback (- increase forward-increase) + flow_1 (if (pos? forward-increase) + (assoc-in flow [vn0 vn1] (+ forward-flow forward-increase)) + flow) + flow_2 (if (pos? pushback) + (assoc-in flow_1 [vn1 vn0] (- reverse-flow pushback)) + flow_1)] + (cond (> pushback reverse-flow) (throw (java.lang.RuntimeException. + (str "Path augmentation failure: " vn0 " " vn1))) + (> (count path) 2) (recur flow_2 capacity (next path) increase) + :else flow_2))) + +(defn edmonds-karp + "Computes the maximum flow on a network, using the edmonds-karp algorithm. + Neighbors is a function that returns the outgoing neighbor vertices of a + vertex. Incoming is a function that returns the incoming neighbor vertices + for a vertex. Capacity is a function of two vertices that returns the + capacity on the edge between them. Source and sink are the unique vertices + which supply and consume flow respectively. + + Returns a vector [flow value], where flow is an adjacency map that represents + flows between vertices, and value is the quantity of flow passing from source + to sink." + ([neighbors incoming capacity source sink] + (edmonds-karp neighbors incoming capacity source sink {})) + ([neighbors incoming capacity source sink flow] + (if-let [path (bf-find-augmenting-path neighbors + incoming capacity flow source sink)] + (recur neighbors incoming capacity source sink + (augment-along-path flow capacity path + (min-weight-along-path + path (partial residual-capacity capacity flow)))) + (let [value (reduce + (vals (get flow source)))] + [flow value])))) \ No newline at end of file diff --git a/test/loom/test/flow.clj b/test/loom/test/flow.clj new file mode 100644 index 0000000..b3cf396 --- /dev/null +++ b/test/loom/test/flow.clj @@ -0,0 +1,55 @@ +(ns loom.test.flow + (:use [loom.graph] :reload) + (:use [loom.flow] + [loom.alg :only [max-flow]] + [clojure.test])) + + +;; Trivial case +(def g0 + (weighted-digraph + [:s :t 100])) + +;; From Cormen et al. Algorithms, 3 ed. p. 726-727 +(def g1 + (weighted-digraph + [:s :v1 16] + [:s :v2 13] + [:v1 :v3 12] + [:v2 :v1 4] + [:v2 :v4 14] + [:v3 :v2 9] + [:v3 :t 20] + [:v4 :v3 7] + [:v4 :t 4])) + +;; Source and sink disconnected +(def g2 + (weighted-digraph + [:s :a 5] + [:b :t 10])) + + +(deftest edmonds-karp-test + (are [max-value network] (let [[flow value] (edmonds-karp (neighbors network) + (incoming network) + (weight network) + :s :t)] + (and (= max-value value) + (is-admissible-flow? flow (weight network) + :s :t))) + 23 g1 + 100 g0 + 0 g2)) + + +(deftest max-flow-convenience-test + (are [max-value network] + (let [[flow value] (max-flow (weighted-digraph network) :s :t)] + (and (= max-value value) + (is-admissible-flow? flow (weight network) :s :t))) + 23 g1)) + + + + From 0edb1f9f83aefa1bf29a5febdeacfc95759ea59c Mon Sep 17 00:00:00 2001 From: Aysylu Date: Wed, 17 Apr 2013 00:10:33 -0400 Subject: [PATCH 02/11] added bellman-ford algorithm and tests --- project.clj | 2 +- src/loom/alg.clj | 81 +++++++++++++++++++++++++++++++++++++++++- test/loom/test/alg.clj | 76 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) diff --git a/project.clj b/project.clj index 9c71e40..e00e862 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ (defproject jkkramer/loom "0.2.0" :description "Graph library for Clojure" :author "Justin Kramer" - :dependencies [[org.clojure/clojure "1.3.0"]]) + :dependencies [[org.clojure/clojure "1.5.1"]]) diff --git a/src/loom/alg.clj b/src/loom/alg.clj index c595bf4..2e9cb42 100644 --- a/src/loom/alg.clj +++ b/src/loom/alg.clj @@ -158,11 +158,90 @@ can use these functions." [g start end] (first (dijkstra-path-dist g start end))) +(defn- can-relax-edge? + "Test for whether we can improve the shortest path to v found so far + by going through u." + [[u v :as edge] weight costs] + (let [vd (get costs v) + ud (get costs u) + sum (+ ud weight)] + (if (> vd sum) + true + false))) + +(defn- relax-edge + "If there's a shorter path from s to v via u, + update our map of estimated path costs and + map of paths from source to vertex v" + [[u v :as edge] weight [costs paths :as estimates]] + (let [ud (get costs u) + sum (+ ud weight)] + (if (can-relax-edge? edge weight costs) + [(assoc costs v sum) (assoc paths v u)] + estimates + ))) + +(defn- relax-edges + "Performs edge relaxation on all edges in weighted directed graph" + [g start estimates] + (->> (edges g) + (reduce (fn [estimates [u v :as edge]] + (relax-edge edge (wt g u v) estimates)) + estimates))) + +(defn- init-estimates + "Initializes path cost estimates and paths from source to all vertices, + for Bellman-Ford algorithm" + [graph start] + (let [nodes (disj (nodes graph) start) + path-costs {start 0} + paths {start nil} + infinities (repeat Double/POSITIVE_INFINITY) + nils (repeat nil) + init-costs (interleave nodes infinities) + init-paths (interleave nodes nils)] + [(apply assoc path-costs init-costs) + (apply assoc paths init-paths)])) + ;;; ;;; Graph algorithms ;;; +(defn bellman-ford + "Given a weighted, directed graph G = (V, E) with source start, + the Bellman-Ford algorithm produces map of single source shortest paths and their costs + if no negative-weight cycle that is reachable from the source exits, + and false otherwise, indicating that no solution exists." + [g start] + (let [initial-estimates (init-estimates g start) + ;relax-edges is calculated for all edges V-1 times + [costs paths] (reduce (fn [estimates _] + (relax-edges g start estimates)) + initial-estimates + (-> g nodes count dec range)) + edges (edges g)] + (if (some + (fn [[u v :as edge]] + (can-relax-edge? edge (wt g u v) costs)) + edges) + false + [costs + (->> (keys paths) + ;remove vertices that are unreachable from source + (remove #(= Double/POSITIVE_INFINITY (get costs %))) + (reduce + (fn [final-paths v] + (assoc final-paths v + ; follows the parent pointers + ; to construct path from source to node v + (loop [node v + path ()] + (if node + (recur (get paths node) (cons node path)) + path)))) + {}))]))) + (defn dag? "Return true if g is a directed acyclic graph" [g] @@ -324,4 +403,4 @@ can use these functions." [#{} #{}] coloring))) -;; TODO: MST, coloring, matching, etc etc \ No newline at end of file +;; TODO: MST, coloring, matching, etc etc diff --git a/test/loom/test/alg.clj b/test/loom/test/alg.clj index 589fb6a..d573d02 100644 --- a/test/loom/test/alg.clj +++ b/test/loom/test/alg.clj @@ -67,6 +67,25 @@ :g [:f] :h [:g :d]})) +;; Weighted directed graph with a negative-weight cycle +;; which is reachable from sources :a, :b, :d, and :e. +;; http://www.seas.gwu.edu/~simhaweb/alg/lectures/module9/module9.html +(def g11 + (weighted-digraph [:a :b 3] + [:b :c 4] + [:b :d 5] + [:d :e 2] + [:e :b -8])) + +;; Weighted directed graph with a non-negative-weight cycle, +;; similar to g11, but with the edge [:e :b] reweighed. +(def g12 + (weighted-digraph [:a :b 3] + [:b :c 4] + [:b :d 5] + [:d :e 2] + [:e :b -7])) + (deftest depth-first-test (are [expected got] (= expected got) #{1 2 3 5 6 7} (set (pre-traverse g7)) @@ -139,6 +158,63 @@ ;; TODO: the rest )) +(deftest bellman-ford-test + (are [expected graph start] + (= expected (bellman-ford graph start)) + + false g11 :a + false g11 :b + [{:e Double/POSITIVE_INFINITY, + :d Double/POSITIVE_INFINITY, + :b Double/POSITIVE_INFINITY, + :a Double/POSITIVE_INFINITY, + :c 0}{:c [:c]}] g11 :c + false g11 :d + false g11 :e + [{:e 10, + :d 8, + :b 3, + :c 7, + :a 0} + {:a [:a], + :c [:a :b :c], + :b [:a :b], + :d [:a :b :d], + :e [:a :b :d :e]}] g12 :a + [{:e 7, + :d 5, + :c 4, + :a Double/POSITIVE_INFINITY, + :b 0} + {:b [:b], + :c [:b :c], + :d [:b :d], + :e [:b :d :e]}] g12 :b + [{:e Double/POSITIVE_INFINITY, + :d Double/POSITIVE_INFINITY, + :b Double/POSITIVE_INFINITY, + :a Double/POSITIVE_INFINITY, + :c 0} + {:c [:c]}] g12 :c + [{:e 2, + :b -5, + :c -1, + :a Double/POSITIVE_INFINITY, + :d 0} + {:d [:d], + :c [:d :e :b :c], + :b [:d :e :b], + :e [:d :e]}] g12 :d + [{:d -2, + :b -7, + :c -3, + :a Double/POSITIVE_INFINITY, + :e 0} + {:e [:e], + :c [:e :b :c], + :b [:e :b], + :d [:e :b :d]}] g12 :e)) + (deftest bipartite-test (are [expected got] (= expected got) {0 1, 1 0, 5 0, 2 1, 3 1, 4 0} (bipartite-color g6) From ad35a6b31042d6c3c0d8eb55f5deebacb9772897 Mon Sep 17 00:00:00 2001 From: Robert Lachlan Date: Mon, 22 Apr 2013 15:38:30 -0700 Subject: [PATCH 03/11] Updated readme, project.clj --- README.md | 8 +++++--- project.clj | 5 +++-- src/loom/graph.clj | 3 +++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index aaa8d95..329fca5 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,15 @@ **Caveat coder**: this lib is alpha-stage. The API may change in future versions. -That said, feedback welcome. Email: [jkkramer@gmail.com](mailto:jkkramer@gmail.com), Twitter: [jkkramer](http://twitter.com/jkkramer) +This library is based off of Justin Kramer's Loom library. + +Contact: [robertlachlan@gmail.com](mailto:robertlachlan@gmail.com). ## Usage ### Leiningen/Clojars [group-id/name version] - [jkkramer/loom "0.2.0"] + [haeffalump/loom "0.3.0-SNAPSHOT"] ### Namespaces @@ -139,4 +141,4 @@ Nothing but Clojure. There is optional support for visualization via [GrapViz](h Copyright (C) 2010 Justin Kramer jkkramer@gmail.com -Distributed under the Eclipse Public License, the same as Clojure. +Distributed under the [Eclipse Public License](http://opensource.org/licenses/eclipse-1.0.php), the same as Clojure. diff --git a/project.clj b/project.clj index c18e47f..90ac2b5 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,5 @@ -(defproject jkkramer/loom "0.2.0" +(defproject heffalump/loom "0.3.0-SNAPSHOT" :description "Graph library for Clojure" - :author "Justin Kramer" + :license {:name "Eclipse Public License" + :url "http://www.eclipse.org/legal/epl-v10.html"} :dependencies [[org.clojure/clojure "1.4.0"]]) diff --git a/src/loom/graph.clj b/src/loom/graph.clj index fae5c71..7a8205b 100644 --- a/src/loom/graph.clj +++ b/src/loom/graph.clj @@ -338,6 +338,9 @@ on adjacency lists." (defrecord WeightedFlyGraph [fnodes fedges fneighbors fweight start]) (defrecord WeightedFlyDigraph [fnodes fedges fneighbors fincoming fweight start]) +;; Deprecate the flygraphs? Instead provide interfaces on algorithms to +;; run the algorithm on + (extend FlyGraph Graph default-flygraph-graph-impl) From 09f84cd5e03c879569d1c173ddcdef55912a2fd9 Mon Sep 17 00:00:00 2001 From: Robert Lachlan Date: Tue, 23 Apr 2013 15:03:28 -0700 Subject: [PATCH 04/11] Added clojure 1.5 as dev dependency --- project.clj | 4 +++- src/loom/graph.clj | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/project.clj b/project.clj index 90ac2b5..3e4b3c6 100644 --- a/project.clj +++ b/project.clj @@ -2,4 +2,6 @@ :description "Graph library for Clojure" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.4.0"]]) + :dependencies [[org.clojure/clojure "1.4.0"]] + :profiles {:dev + {:dependencies [[org.clojure/clojure "1.5.1"]]}}) diff --git a/src/loom/graph.clj b/src/loom/graph.clj index 7a8205b..71113da 100644 --- a/src/loom/graph.clj +++ b/src/loom/graph.clj @@ -7,8 +7,6 @@ on adjacency lists." loom.graph (:use [loom.alg-generic :only [bf-traverse]])) -;(set! *warn-on-reflection* true) - ;;; ;;; Protocols ;;; @@ -34,6 +32,8 @@ on adjacency lists." (defprotocol WeightedGraph (weight [g] [g n1 n2] "Return weight of edge [n1 n2] or (partial weight g)")) +(defprotocol VertexLabelled) + ;; Variadic wrappers (defn add-nodes From d1bb50b62e5286e5790e06e1bbbd39e8addc5a90 Mon Sep 17 00:00:00 2001 From: Aysylu Date: Sat, 27 Apr 2013 15:37:15 -0400 Subject: [PATCH 05/11] added logo and updated README to display the logo --- README.md | 1 + doc/loom_logo.svg | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 doc/loom_logo.svg diff --git a/README.md b/README.md index aaa8d95..abcd9fb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Loom +![alt text](https://github.com/aysylu/loom/raw/master/doc/loom_logo.svg "Loom logo") **Caveat coder**: this lib is alpha-stage. The API may change in future versions. diff --git a/doc/loom_logo.svg b/doc/loom_logo.svg new file mode 100644 index 0000000..c2e7906 --- /dev/null +++ b/doc/loom_logo.svg @@ -0,0 +1,4 @@ + + + + From b79bca255f957140b0e4eab3956fa3774d8bd3cd Mon Sep 17 00:00:00 2001 From: Aysylu Date: Sat, 27 Apr 2013 15:38:33 -0400 Subject: [PATCH 06/11] fixed path to logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abcd9fb..9e32d4d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Loom -![alt text](https://github.com/aysylu/loom/raw/master/doc/loom_logo.svg "Loom logo") +![alt text](https://github.com/aysylu/loom/tree/master/doc/loom_logo.svg "Loom logo") **Caveat coder**: this lib is alpha-stage. The API may change in future versions. From 1f48c033f55cb030c530a37576b6b2ccee790e76 Mon Sep 17 00:00:00 2001 From: Aysylu Date: Sat, 27 Apr 2013 15:40:17 -0400 Subject: [PATCH 07/11] fix for path to logo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e32d4d..ecd141d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Loom -![alt text](https://github.com/aysylu/loom/tree/master/doc/loom_logo.svg "Loom logo") +![alt text](https://raw.github.com/aysylu/loom/master/doc/loom_logo.svg "Loom logo") **Caveat coder**: this lib is alpha-stage. The API may change in future versions. From 5630ab5fc0854c137516f2634297290c8448ea7e Mon Sep 17 00:00:00 2001 From: Aysylu Date: Sat, 27 Apr 2013 15:45:30 -0400 Subject: [PATCH 08/11] svg file is not rendering, using png instead --- README.md | 2 +- doc/loom_logo.png | Bin 0 -> 8252 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 doc/loom_logo.png diff --git a/README.md b/README.md index ecd141d..de3e914 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Loom -![alt text](https://raw.github.com/aysylu/loom/master/doc/loom_logo.svg "Loom logo") +![alt text](https://raw.github.com/aysylu/loom/master/doc/loom_logo.png "Loom logo") **Caveat coder**: this lib is alpha-stage. The API may change in future versions. diff --git a/doc/loom_logo.png b/doc/loom_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..97fdd0bba67e60e3c4cd51807bf6fe92c31da498 GIT binary patch literal 8252 zcmaKRby$>N&^C&6BP@slQcHJ;q%;dsOCyVPw}iAH-3@{)wM+NXog&>KNP~1pH{as# z`@X;4>wW&1o$H*LduE<{&bgjzCrnjY4iB3G8wCXg4$mhBSkdyo!$Kn&)~jW^I{-L8b}WCwM1SBN<6x2=vSkU${>{@T0PM%TBK6xh&%R}*+21G52?M@ z`-kyQypFVV&5A%Sc6WyT+kD68js3sax3^hlWP3f@OQyfp>-{gnDC+;>fsC-9`(Fe~ ziE65xf2t&94jK~VpZHhyyXya&8glg=`PkgM|K1X}MD_I8V>!k~#Q%dwUDV>PO=55R zGf|cP?Y35-QGR1;*Bl$<%XlJw3i+r$Gjn`)wtVgB;se1Rf%us3UpIOwO0#X}GJF*i zTD_2Jky;=(N9m@~!`aH|&uI>zT)EoOC$s&I@u4CnSO*q<9!FfiziV@QU1YTNWI0AR zRBIr+pB(=c`D*j-(KUs=zZlr=^HyQZ@MVj2m+G^2Q5?e3~r*p7@jDrl? z9hWL~#u2aoCK{40nCdRKoIkFUfUTf*oY>CkmM&=Ki8vjS$ldAOPJI;xo*#WRQce~V zXK1JI(y?wuakY_j)qQ&r&-+^v!a@Qt$&tm)f#MbCz)(%zuAf2{f?D+b7ST@Z2OJOCzFDK8f$A4W+un83ODUIcr0#Dr_et|q;hR^d-zuQtQr?;_CJ}U$jV2Zuj7D{m-LLhWCLE5yzWME0O5qN5{B5}-%kVk;Q59iy z&d)0Cn{VuoZfjY1fK9)S@-rVaQEsxK^Wd*e7pK?PuntiX8!ivk=S2;eK%AWg^$pe# zyo0+UP*WL`pRVDuK*TiUsaY_(ON4RTPb%q!RriC6lq$rkEPW z60@uV5(k+Gt^O619GOAkTb^y++K+*Q2O=?QRRcaVmn$TJy`_DvpHj>2sdWV9G%zeD&Yk0 zbU%hI&%P_r57VM^wi#?)FV0-kj3Hd8zQiim=pqbSPjT zI5AJ&&xaoCFA34}4T-zU>+9H4w@~*658L!SMo)kseJW|m>4UqPr1-C_#hanQyR=YE zqA}}Du4TAfrS%_=QZJrkyW3$O=@mCa5cFHMGK3kXBu%5YVnx>?9U_NkP-X^=oJ=D} zV{r2_&8*Q{TLMjox`;wwgxvAvT1Cn#5SB3FneVTq{V>l9beic%h?PSNDhjonQa6B% zEpgBqV5|$|s+*m(N^IEEm{SD#TRyP^6r^5cJ(FP&FWI=Qqk{G=adM@rNFN%>@{xf! zzYfR43y3 zpY7zE=ZGuaCrx*xB z(~Ys2$jS7g!^9A1n%2{;))QJ1=keq>_-!&i5ReW}Av292@toGh1oCy!j4L8^mknGx z^J!)R-=nc=h{Ic0k|-PHR-)ESWzbl6oxYU}6`mE>(E&>Oh3Vrh&j>Ml@_xKaXEL0% zv-twlsc^yYh+4lcRje8?-x)n3R%X)oY$`RpI>=wYN$K;ZTO!O!0+d*gwE53KOf=#m z30Ov{T<_i_Wx}n4s?YGGS#j@R+BUF(!_8)wr7Cim!Yx&T$MjfeK4H((t5Q53(vpd% z1{ufR4}&I57PL>h1er0l*s!pN%JS1jjGAfp3vZ+42ulTLowtS=(Ynej;@WxueCyuJ9Fw#VU!F8dA46gcRH_R*cfwJ7}IvfccPS)qCYDBhnW3P_3)#gzxsZc&I8cwGKyNFPl#kH1#2D&N5Yz!Tbhkx?c}pPF%KD zVqCC!=xbraANp* zJ*DC|x6%_oVVtAAAI0WeG)X?md}bt$GW5#S2uya!VN#U$tm zx8W~F`4l!OI5TK^Hzs)vFS_hqX+W-=4n zjsx|qf7k2dcuG1`CS$+gh;O40N&6ve4nU|QpP6|-*E}X z48M1ncnV=GCY?*gvBMN{fatf0LW}hAS>_1qmP1}P?^d({8Sx;B1dcLZmRjKmj{WHQ zlgb|!%0D0aW#hNN498JN-p4QLub?zy$=i@`f>SPEUR%y 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 literal 0 HcmV?d00001 From 887983a5944b499edfebb3c93cd42e3520cf1611 Mon Sep 17 00:00:00 2001 From: Aysylu Date: Sat, 27 Apr 2013 15:47:51 -0400 Subject: [PATCH 09/11] fixed title --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index de3e914..7d82480 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -# Loom -![alt text](https://raw.github.com/aysylu/loom/master/doc/loom_logo.png "Loom logo") +![Loom logo](https://raw.github.com/aysylu/loom/master/doc/loom_logo.png "Loom") **Caveat coder**: this lib is alpha-stage. The API may change in future versions. From 3c0b3420253d2142579ebf0e74b4bb8692a38670 Mon Sep 17 00:00:00 2001 From: Aysylu Date: Sat, 27 Apr 2013 15:52:42 -0400 Subject: [PATCH 10/11] updated contact info --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d82480..5af67a9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Caveat coder**: this lib is alpha-stage. The API may change in future versions. -That said, feedback welcome. Email: [jkkramer@gmail.com](mailto:jkkramer@gmail.com), Twitter: [jkkramer](http://twitter.com/jkkramer) +That said, feedback welcome. Github: [aysylu](https://github.com/aysylu), [Heffalump](https://github.com/Heffalump), Email: [aysylu7@gmail.com](mailto:aysylu7@gmail.com), Twitter: [aysylu22](http://twitter.com/aysylu22) ## Usage From bfe5cabe8a49e1873cc2980d03cb77e1771653df Mon Sep 17 00:00:00 2001 From: Aysylu Date: Sat, 27 Apr 2013 15:55:08 -0400 Subject: [PATCH 11/11] updated how email is displayed --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5af67a9..48aec3f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Caveat coder**: this lib is alpha-stage. The API may change in future versions. -That said, feedback welcome. Github: [aysylu](https://github.com/aysylu), [Heffalump](https://github.com/Heffalump), Email: [aysylu7@gmail.com](mailto:aysylu7@gmail.com), Twitter: [aysylu22](http://twitter.com/aysylu22) +That said, feedback welcome. Github: [aysylu](https://github.com/aysylu), [Heffalump](https://github.com/Heffalump), Email: [aysylu7 [at] gmail [dot] com](mailto:aysylu7@gmail.com), Twitter: [aysylu22](http://twitter.com/aysylu22) ## Usage