turbine-graph 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ group :extras do
4
+ gem 'guard', require: false
5
+ gem 'guard-rspec', require: false
6
+ gem 'ruby_gntp', require: false
7
+ gem 'rb-fsevent', '~> 0.9.1', require: false
8
+ gem 'coolline', require: false
9
+ gem 'simplecov', require: false
10
+ gem 'yard', '>= 0.8', require: false
11
+ gem 'yard-tomdoc', '>= 0.5', require: false
12
+ gem 'pry', require: false
13
+ end
14
+
15
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,6 @@
1
+ guard 'rspec', :version => 2 do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ watch(%r{^spec/factories/.*}) { "spec" }
6
+ end
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2012, Anthony Williams and Quintel Intelligence
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ * Neither the name of the Anthony Williams nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL ANTHONY WILLIAMS BE LIABLE FOR ANY DIRECT,
22
+ INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
23
+ BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
25
+ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
27
+ EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # Turbine [![Build Status](https://secure.travis-ci.org/quintel/turbine.png)](http://travis-ci.org/quintel/turbine)
2
+
3
+ An in-memory directed graph written in Ruby to model an energy flow system,
4
+ a family tree, or whatever you like!
5
+
6
+ ## Quick tour
7
+
8
+ We start the console with `rake console:stub` and load the example graph:
9
+
10
+ ```ruby
11
+ graph = Turbine.energy_stub
12
+ # => #<Turbine::Graph (16 nodes, 16 edges)>
13
+ ```
14
+
15
+ ### Searching
16
+
17
+ Now you can search for a node:
18
+
19
+ ```ruby
20
+ graph.node(:space_heater_chp)
21
+ # => #<Turbine::Node key=:space_heater_chp>
22
+ ```
23
+
24
+ It will return nil when the collection is empty, or if no node with the given
25
+ key exists:
26
+
27
+ ```ruby
28
+ graph.node(:bob_ross)
29
+ # => nil
30
+ ```
31
+
32
+ ### Traversing the graph
33
+
34
+ Please see the [Terminology](#terminology) section if you are confused by the
35
+ use of **in** and **out**.
36
+
37
+ #### Adjacent nodes
38
+
39
+ Traverse the graph by requesting the inward nodes of a node:
40
+
41
+ ```ruby
42
+ graph.node(:space_heater_chp).in.to_a
43
+ # => [#<Turbine::Node>, #<Turbine::Node>, ...]
44
+ ```
45
+
46
+ Traverse the graph by requesting the outward nodes:
47
+
48
+ ```ruby
49
+ graph.node(:space_heater_chp).out.to_a
50
+ # => [#<Turbine::Node>, #<Turbine::Node>, ...]
51
+ ```
52
+
53
+ #### Filtering nodes
54
+
55
+ If you have a node and you want to get all the inward or outward nodes that
56
+ have a certain label, you can use a filter:
57
+
58
+ ```ruby
59
+ graph.node(:space_heater_chp).out(:electricity).to_a
60
+ # => [#<Turbine::Node key=:useful_demand_elec>]
61
+
62
+ graph.node(:space_heater_chp).out(:heat).to_a
63
+ # => [<Turbine::Node key=:useful_demand_heat>}>]
64
+ ```
65
+
66
+ #### Traversing edges
67
+
68
+ You can do the same for edges with `in_edges` and `out_edges`:
69
+
70
+ ```ruby
71
+ graph.node(:space_heater_chp).in_edges.to_a
72
+ # => [ #<Turbine::Edge :space_heater_coal -:heat-> :useful_demand_heat>,
73
+ # #<Turbine::Edge :space_heater_gas -:heat-> :useful_demand_heat>,
74
+ # #<Turbine::Edge :space_heater_oil -:heat-> :useful_demand_heat>,
75
+ # #<Turbine::Edge :space_heater_chp -:heat-> :useful_demand_heat> ]
76
+ ```
77
+
78
+ #### Chaining
79
+
80
+ You can also chain and step through the connections:
81
+
82
+ ```ruby
83
+ node = graph.nodes.first
84
+ node.in.in.to_a
85
+ # => [ #<Turbine::Node key=:final_demand_coal>,
86
+ # #<Turbine::Node key=:final_demand_gas>,
87
+ # #<Turbine::Node key=:final_demand_oil> ]
88
+ ```
89
+
90
+ #### Ancestors and Descendants
91
+
92
+ Alternatively, you can recursively fetch all ancestors or descendants of a
93
+ Node:
94
+
95
+ ```ruby
96
+ enum = node.ancestors.to_a
97
+ # => [#<Turbine::Node>, #<Turbine::Node>, ...]
98
+ ```
99
+
100
+ Just like with `in` and `out`, you may opt to filter the traversed nodes by
101
+ the label of the connecting edge:
102
+
103
+ ```ruby
104
+ enum = node.descendants(:likes)
105
+ # => #<Enumerator: ...>
106
+ ```
107
+
108
+ Ancestors and descendants are fetched using a breadth-first algorithm, but
109
+ depth-first is also available:
110
+
111
+ ```ruby
112
+ enum = Turbine::Traversal::DepthFirst(node, :in).to_enum
113
+ # => #<Enumerator: ...>
114
+ ```
115
+
116
+ Each adjacent node is visited no more than once during the traversal, i.e.
117
+ loops are not followed.
118
+
119
+ ### Properties / Attributes
120
+
121
+ You can set all kind of properties on a node:
122
+
123
+ ```ruby
124
+ node = graph.nodes.first
125
+ node.properties
126
+ # => {} # no properties set!
127
+
128
+ node.get(:preset_demand)
129
+ # => nil # no property called :preset_demand set!
130
+
131
+ node.set(:preset_demand, 1_000)
132
+ # => 1000
133
+
134
+ node.properties
135
+ # => {:preset_demand=>1000}
136
+ ```
137
+
138
+ ## Terminology
139
+
140
+ #### Node
141
+
142
+ In graph theory, the term **Node** and **Vertex** are used interchangeably.
143
+ Since we prefer shorter words over longer: we use node.
144
+
145
+ #### Edges
146
+
147
+ An **edge** (or sometimes called an **arc**) is a connection between two
148
+ nodes.
149
+
150
+ #### Directed graph
151
+
152
+ Turbine is a directed graph, which means that the connection between two
153
+ nodes always has a direction: it either goes from A to B or the other way
154
+ round.
155
+
156
+ #### In and out
157
+
158
+ When Node A is connected to Node B:
159
+
160
+ A --> B
161
+
162
+ A is said to be the **ancestor** of B, and B is called the **descendant** of
163
+ A. Since we like to keep things as short as possible, we choose **in** and
164
+ **out**: `A.out` results in `B`, and `B.in` results in `A`.
165
+
166
+ Hence, we have the following truth table:
167
+
168
+ | in | out
169
+ -----+-----+------
170
+ A | nil | B
171
+ -----+-----+------
172
+ B | A | nil
173
+
174
+ Still, it is up to the user to define what the direction signifies: in the
175
+ case of an energy graph: the energy flows from a coal plant to the electricity
176
+ grid. (some might argue that the demand flows from the grid to the power
177
+ plant).
178
+
179
+ In the case of the family graph, it is the ancestry of people:
180
+
181
+ parent -:child-> child
182
+
183
+ If the relation is equal, there are two edges defined and a symmetrical
184
+ relationship exists (Turbine does not support bi-directional edges):
185
+
186
+ person1 -:spouse-> person2
187
+ person2 -:spouse-> person1
188
+
189
+ i.e., person1 <-> person2
data/Rakefile ADDED
@@ -0,0 +1,126 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+
4
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
5
+ require 'turbine/version'
6
+
7
+ CLOBBER.include %w( pkg *.gem doc coverage measurements )
8
+
9
+ # Helpers --------------------------------------------------------------------
10
+
11
+ require 'date'
12
+
13
+ def replace_header(head, header_name, value)
14
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{value}'" }
15
+ end
16
+
17
+ # Build Tasks ----------------------------------------------------------------
18
+
19
+ desc 'Build the gem, and push to Github'
20
+ task :release => :build do
21
+ unless `git branch` =~ /^\* master$/
22
+ puts "You must be on the master branch to release!"
23
+ exit!
24
+ end
25
+
26
+ sh "git commit --allow-empty -a -m 'Release #{Turbine::VERSION}'"
27
+ sh "git tag v#{Turbine::VERSION}"
28
+ sh "git push origin master"
29
+ sh "git push origin v#{Turbine::VERSION}"
30
+
31
+ puts "Push to Rubygems.org with"
32
+ puts " gem push pkg/turbine-#{Turbine::VERSION}.gem"
33
+ end
34
+
35
+ desc 'Builds the gem'
36
+ task :build => [:gemspec] do
37
+ sh "mkdir -p pkg"
38
+ sh "gem build turbine.gemspec"
39
+ sh "mv turbine-graph-#{Turbine::VERSION}.gem pkg"
40
+ end
41
+
42
+ desc 'Create a fresh gemspec'
43
+ task :gemspec => :validate do
44
+ gemspec_file = File.expand_path('../turbine.gemspec', __FILE__)
45
+
46
+ # Read spec file and split out the manifest section.
47
+ spec = File.read(gemspec_file)
48
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
49
+
50
+ # Replace name version and date.
51
+ replace_header head, :name, 'turbine-graph'
52
+ replace_header head, :rubyforge_project, 'turbine-graph'
53
+ replace_header head, :version, Turbine::VERSION
54
+ replace_header head, :date, Date.today.to_s
55
+
56
+ # Determine file list from git ls-files.
57
+ files = `git ls-files`.
58
+ split("\n").
59
+ sort.
60
+ reject { |file| file =~ /^\./ }.
61
+ reject { |file| file =~ /^(doc|pkg|spec|tasks)/ }
62
+
63
+ # Format list for the gemspec.
64
+ files = files.map { |file| " #{file}" }.join("\n")
65
+
66
+ # Piece file back together and write.
67
+ manifest = " s.files = %w[\n#{files}\n ]\n"
68
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
69
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
70
+
71
+ puts "Updated #{gemspec_file}"
72
+ end
73
+
74
+ task :validate do
75
+ unless Dir['lib/*'] - %w(lib/turbine.rb lib/turbine)
76
+ puts 'The lib/ directory should only contain a turbine.rb file, and a ' \
77
+ 'turbine/ directory'
78
+ exit!
79
+ end
80
+
81
+ unless Dir['VERSION*'].empty?
82
+ puts 'A VERSION file at root level violates Gem best practices'
83
+ exit!
84
+ end
85
+ end
86
+
87
+ # Coverage -------------------------------------------------------------------
88
+
89
+ task :coverage do
90
+ ENV['COVERAGE'] = 'true'
91
+ exec 'bundle exec rspec'
92
+ end
93
+
94
+ # Documentation --------------------------------------------------------------
95
+
96
+ begin
97
+ require 'yard'
98
+ require 'yard-tomdoc'
99
+ YARD::Rake::YardocTask.new do |doc|
100
+ doc.options << '--no-highlight'
101
+ end
102
+ rescue LoadError
103
+ desc 'yard task requires that the yard gem is installed'
104
+ task :yard do
105
+ abort 'YARD is not available. In order to run yard, you must: gem ' \
106
+ 'install yard'
107
+ end
108
+ end
109
+
110
+ # Console --------------------------------------------------------------------
111
+
112
+ namespace :console do
113
+ task :run do
114
+ command = system("which pry > /dev/null 2>&1") ? 'pry' : 'irb'
115
+ exec "#{ command } -I./lib -r./lib/turbine.rb"
116
+ end
117
+
118
+ desc 'Open a pry or irb session with a stub graph on `Turbine.stub`'
119
+ task :stub do
120
+ command = system("which pry > /dev/null 2>&1") ? 'pry' : 'irb'
121
+ exec "#{ command } -I./lib -r./lib/turbine.rb -r./examples/family.rb -r./examples/energy.rb"
122
+ end
123
+ end
124
+
125
+ desc 'Open a pry or irb session preloaded with Turbine'
126
+ task console: ['console:run']
@@ -0,0 +1,80 @@
1
+ require 'turbine'
2
+
3
+ module Turbine
4
+
5
+ # This is a small Graph example, similar to the structure that is used
6
+ # in ETEngine in households.
7
+ #
8
+ # Overview:
9
+ # +------------------------------------------------------------------+
10
+ # | ud_heat <- sh_coal <- fd_coal |
11
+ # | \\\--- sh_oil <- fd_oil |
12
+ # | \\--- sh_gas <- fd_gas |
13
+ # | \--- sh_chp <--/ |
14
+ # | ud_elec <--/ |
15
+ # | \--- sh_elec <- fd_elec <- lv <- mv <- hv <- coal plant |
16
+ # | \--- elec_import |
17
+ # +------------------------------------------------------------------+
18
+ #
19
+ # Abbreviations used:
20
+ # ud = useful_demand
21
+ # sh = space_heater
22
+ # fd = final_demand
23
+ # hv = high voltage
24
+ # mv = medium voltage
25
+ # lv = low voltage
26
+ # elec = electricity
27
+ #
28
+ def self.energy_stub
29
+ graph = Turbine::Graph.new
30
+
31
+ # Nodes ------------------------------------------------------------------
32
+
33
+ useful_demand_heat = graph.add(Turbine::Node.new(:useful_demand_heat))
34
+ useful_demand_elec = graph.add(Turbine::Node.new(:useful_demand_elec))
35
+
36
+ space_heater_coal = graph.add(Turbine::Node.new(:space_heater_coal))
37
+ space_heater_gas = graph.add(Turbine::Node.new(:space_heater_gas))
38
+ space_heater_oil = graph.add(Turbine::Node.new(:space_heater_oil))
39
+ space_heater_chp = graph.add(Turbine::Node.new(:space_heater_chp))
40
+ space_heater_elec = graph.add(Turbine::Node.new(:space_heater_elec))
41
+
42
+ final_demand_coal = graph.add(Turbine::Node.new(:final_demand_coal))
43
+ final_demand_gas = graph.add(Turbine::Node.new(:final_demand_gas))
44
+ final_demand_oil = graph.add(Turbine::Node.new(:final_demand_oil))
45
+ final_demand_elec = graph.add(Turbine::Node.new(:final_demand_elec))
46
+
47
+ lv_network = graph.add(Turbine::Node.new(:lv_network))
48
+ mv_network = graph.add(Turbine::Node.new(:mv_network))
49
+ hv_network = graph.add(Turbine::Node.new(:hv_network))
50
+
51
+ coal_plant = graph.add(Turbine::Node.new(:coal_plant))
52
+ elec_import = graph.add(Turbine::Node.new(:elec_import))
53
+
54
+ # Edges ------------------------------------------------------------------
55
+
56
+ elec_import.connect_to(hv_network, :electricity)
57
+ coal_plant.connect_to(hv_network, :electricity)
58
+
59
+ hv_network.connect_to(mv_network, :electricity)
60
+ mv_network.connect_to(lv_network, :electricity)
61
+ lv_network.connect_to(final_demand_elec, :electricity)
62
+
63
+ final_demand_elec.connect_to(space_heater_elec, :electricity)
64
+ final_demand_coal.connect_to(space_heater_coal, :coal)
65
+ final_demand_gas.connect_to(space_heater_gas, :gas)
66
+ final_demand_gas.connect_to(space_heater_chp, :gas)
67
+ final_demand_oil.connect_to(space_heater_oil, :oil)
68
+
69
+ space_heater_coal.connect_to(useful_demand_heat, :heat)
70
+ space_heater_gas.connect_to(useful_demand_heat, :heat)
71
+ space_heater_oil.connect_to(useful_demand_heat, :heat)
72
+
73
+ space_heater_chp.connect_to(useful_demand_heat, :heat)
74
+ space_heater_chp.connect_to(useful_demand_elec, :electricity)
75
+
76
+ space_heater_elec.connect_to(useful_demand_elec, :electricity)
77
+
78
+ graph
79
+ end #self.stub
80
+ end #Module Turbine