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 +15 -0
- data/Guardfile +6 -0
- data/LICENSE +27 -0
- data/README.md +189 -0
- data/Rakefile +126 -0
- data/examples/energy.rb +80 -0
- data/examples/family.rb +125 -0
- data/lib/turbine.rb +29 -0
- data/lib/turbine/algorithms/filtered_tarjan.rb +33 -0
- data/lib/turbine/algorithms/tarjan.rb +50 -0
- data/lib/turbine/edge.rb +94 -0
- data/lib/turbine/errors.rb +74 -0
- data/lib/turbine/graph.rb +113 -0
- data/lib/turbine/node.rb +246 -0
- data/lib/turbine/pipeline/README.mdown +31 -0
- data/lib/turbine/pipeline/dsl.rb +275 -0
- data/lib/turbine/pipeline/expander.rb +67 -0
- data/lib/turbine/pipeline/filter.rb +52 -0
- data/lib/turbine/pipeline/journal.rb +130 -0
- data/lib/turbine/pipeline/journal_filter.rb +71 -0
- data/lib/turbine/pipeline/pump.rb +19 -0
- data/lib/turbine/pipeline/segment.rb +175 -0
- data/lib/turbine/pipeline/sender.rb +51 -0
- data/lib/turbine/pipeline/split.rb +132 -0
- data/lib/turbine/pipeline/trace.rb +55 -0
- data/lib/turbine/pipeline/transform.rb +49 -0
- data/lib/turbine/pipeline/traversal.rb +34 -0
- data/lib/turbine/pipeline/unique.rb +47 -0
- data/lib/turbine/properties.rb +48 -0
- data/lib/turbine/traversal/base.rb +133 -0
- data/lib/turbine/traversal/breadth_first.rb +49 -0
- data/lib/turbine/traversal/depth_first.rb +46 -0
- data/lib/turbine/version.rb +4 -0
- data/turbine.gemspec +84 -0
- metadata +120 -0
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
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 [](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']
|
data/examples/energy.rb
ADDED
@@ -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
|