nodus 0.3.1
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.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +38 -0
- data/LICENSE.txt +20 -0
- data/OPERUM.md +8 -0
- data/README.md +383 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/dia.rb +29 -0
- data/doc/desc.md +191 -0
- data/doc/example.node +89 -0
- data/doc/nodes.rb +77 -0
- data/doc/pipe.svg +97 -0
- data/doc/pipe.txt +4 -0
- data/doc/pipe2.dot +49 -0
- data/doc/pipe2.svg +163 -0
- data/lib/VERSION +1 -0
- data/lib/extensions.rb +162 -0
- data/lib/flexhash.rb +175 -0
- data/lib/nodus.rb +77 -0
- data/lib/nodus/nodes.rb +160 -0
- data/lib/nodus/stream.rb +12 -0
- data/lib/nodus/token.rb +31 -0
- data/lib/nodus/version.rb +6 -0
- data/lib/proplist.rb +142 -0
- data/nodus.gemspec +106 -0
- data/spec.md +60 -0
- data/test/core/test_flexhash.rb +87 -0
- data/test/core/test_generator.rb +27 -0
- data/test/core/test_node.rb +103 -0
- data/test/core/test_proplist.rb +153 -0
- data/test/helper.rb +107 -0
- metadata +188 -0
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
require 'git'
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
17
|
+
gem.name = "nodus"
|
18
|
+
gem.homepage = "http://github.com/exsig/nodus"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = "Something between a Kahn Process Network and Algorithmic Skeleton for parallel pipelining and signal processing"
|
21
|
+
gem.description = %Q{EXPERIMENTAL. A form of data-flow programming based loosely on Kahn Process Networks. Will allow
|
22
|
+
for setting up operational components that can be pipelined together in a graph. Assumes all
|
23
|
+
components (nodes) are 'online' algorithms with more or less steady-state resource utilization
|
24
|
+
for continuous streams of data.}.gsub(/\s+/,' ')
|
25
|
+
gem.email = "joseph.wecker@exsig.com"
|
26
|
+
gem.authors = ["Joseph Wecker"]
|
27
|
+
|
28
|
+
# (dependencies are defined in the Gemfile)
|
29
|
+
end
|
30
|
+
Jeweler::RubygemsDotOrgTasks.new
|
31
|
+
|
32
|
+
require 'rake/testtask'
|
33
|
+
Rake::TestTask.new(:test) do |test|
|
34
|
+
test.libs << 'lib' << 'test'
|
35
|
+
test.pattern = 'test/**/test_*.rb'
|
36
|
+
test.verbose = true
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Code coverage detail"
|
40
|
+
task :simplecov do
|
41
|
+
ENV['COVERAGE'] = "true"
|
42
|
+
Rake::Task['test'].execute
|
43
|
+
end
|
44
|
+
|
45
|
+
task :default => :test
|
46
|
+
|
47
|
+
require 'rdoc/task'
|
48
|
+
Rake::RDocTask.new do |rdoc|
|
49
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ''
|
50
|
+
|
51
|
+
rdoc.rdoc_dir = 'rdoc'
|
52
|
+
rdoc.title = "nodus #{version}"
|
53
|
+
rdoc.rdoc_files.include('README*')
|
54
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
55
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.1
|
data/dia.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
|
11
|
+
require 'nodus'
|
12
|
+
include Nodus
|
13
|
+
|
14
|
+
class A < Node
|
15
|
+
input :x
|
16
|
+
output :x
|
17
|
+
end
|
18
|
+
|
19
|
+
class B < Node
|
20
|
+
input :x
|
21
|
+
output :x
|
22
|
+
end
|
23
|
+
|
24
|
+
a = A[]
|
25
|
+
b = B[]
|
26
|
+
|
27
|
+
a.add_subscriber(b)
|
28
|
+
|
29
|
+
puts a.to_dot
|
data/doc/desc.md
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
Composition Rules
|
2
|
+
-----------------
|
3
|
+
|
4
|
+
Compositions should be parameterized one way or another- built dynamically and allowing for variable interpolation.
|
5
|
+
|
6
|
+
Most composition nodes are given a `NodeList`, which consists of either:
|
7
|
+
* An ordered list of nodes (although order doesn't always matter)
|
8
|
+
* OR, a design-time function that calculates the list of nodes
|
9
|
+
* OR, a run-time function that calculates the list of nodes depending on token data (in which case we need to require
|
10
|
+
some sort of template / worst-case scenario etc. so that we can determine composition at a higher level? or maybe a
|
11
|
+
range specifier?)
|
12
|
+
* (with port-specifiers for individual nodes if necessary)
|
13
|
+
|
14
|
+
Most accept one or more kernels (== `lambda`, `proc`, `block`, or misc. `class` constant with specific handlers/behavior)
|
15
|
+
|
16
|
+
If a class handler, it will probably want to implement one or more of:
|
17
|
+
- Data Handler
|
18
|
+
- Upstream Exception Handler
|
19
|
+
- Downstream Exception Handler ?
|
20
|
+
- OOB Message Handler (such as N/A)
|
21
|
+
|
22
|
+
### Core Custom Node
|
23
|
+
|
24
|
+
#### Port types:
|
25
|
+
|
26
|
+
`parameter: (optional[<default>] | required)`
|
27
|
+
|
28
|
+
`| input: (operational<output-port[s]> | consumed [control]) x (optional | required)`
|
29
|
+
|
30
|
+
`| output: ( operational<input-port[s]> | generated [control]) x (tap | primary)`
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
##### Parameters
|
35
|
+
- **parameter**(default=nil): w/ optional default... specialized optional or required input port (probably not
|
36
|
+
implemented as actual message channel). Also possibly enforce the fact that it can't be connected to a stream.
|
37
|
+
Possibly composable / or able to be overridden kind of in parallel to other compositions. **optional** or
|
38
|
+
**required**. *These are also outputs. i.e., they are readable.* In fact they are intrinsicly different than normal
|
39
|
+
ports because they must be set before any real data comes through the node.
|
40
|
+
|
41
|
+
##### Inputs
|
42
|
+
- **operational**(out-port[s]): port has paired output port that is stream-synchronized with this input.
|
43
|
+
- **control**: specialized (and implied) end-point used to help node make decisions. e.g., state / out-of-band messages
|
44
|
+
- **consumed**: End-point / Sink. Node reads input but doesn't have corresponding synchronized output.
|
45
|
+
- **optional**: Node can run without this being connected to anything (although not sure if they can connect at some
|
46
|
+
later point in time...)
|
47
|
+
|
48
|
+
##### Outputs
|
49
|
+
- **operational**(in-port[s]): port has paired input port and this output adds to (or passes through) those input tokens.
|
50
|
+
- **controller**: Specialized (and implied) generated port used to help other nodes make decisions. Also state & out of band messages.
|
51
|
+
- **generated**: Origin / Generator. Node generates stream / it has no corresponding input port.
|
52
|
+
- **tap-point**: Output port that can optionally be tapped into (usually meaning it already has a listener within the node).
|
53
|
+
|
54
|
+
|
55
|
+
#### Helpers / Quick Builders
|
56
|
+
|
57
|
+
- **Simple Generator**
|
58
|
+
- **Simple Processor**
|
59
|
+
- **Simple Consumer** = **Simple Fold**
|
60
|
+
- **Simple Projection**
|
61
|
+
|
62
|
+
### Axiomatic
|
63
|
+
|
64
|
+
**Node:**
|
65
|
+
* I=0..n (input ports) and O=0..n (output ports). Most of the time a static number, but sometimes a range of available
|
66
|
+
ports.
|
67
|
+
|
68
|
+
**Connector:**
|
69
|
+
* *AdHoc*
|
70
|
+
* Given an arbitrary list of nodes, allows specifying connection pairs between available inputs/outputs.
|
71
|
+
* Result is a node with all remaining unconnected input and output ports
|
72
|
+
* Can also connect values to parameter inputs
|
73
|
+
* Essentially a curry function
|
74
|
+
|
75
|
+
**Pipe:**
|
76
|
+
* *AdHoc*
|
77
|
+
* Connects member nodes on specified or default ports (specialized connector)
|
78
|
+
* All member nodes except last must have at least one output
|
79
|
+
* All member nodes except for first must have at least one input
|
80
|
+
* (like connector) result defined by unconnected inputs/outputs (usually just one main input and one main output)
|
81
|
+
|
82
|
+
**Concurrent:**
|
83
|
+
* *AdHoc*
|
84
|
+
* Executes members concurrently. Input & output streams are all inputs and outputs of members.
|
85
|
+
|
86
|
+
**Join:**
|
87
|
+
* *Stream-Synchronized*
|
88
|
+
* Token aware- meant to merge parallel branches of a single stream
|
89
|
+
* Unlimited input connections
|
90
|
+
* Listens for NOPs
|
91
|
+
|
92
|
+
**Multiply:**
|
93
|
+
* *Stream-Synchronized*
|
94
|
+
* Single input port split into specified number of output ports
|
95
|
+
* Each branch (implicitly?) has its own parallel context on the token
|
96
|
+
|
97
|
+
**View:**
|
98
|
+
* *Stream-Synchronized*
|
99
|
+
* Given a token, select a different set of fields to be the current context for downstream nodes
|
100
|
+
|
101
|
+
**Filter:**
|
102
|
+
* *Stream-Synchronized* (NOP) OR *Projection* (Drop)
|
103
|
+
* Drop or NOP tokens matching certain criteria
|
104
|
+
|
105
|
+
**Select:**
|
106
|
+
* *Stream-Synchronized* (NOP) OR *Projection* (Drop)
|
107
|
+
* Drop or NOP tokens not matching certain criteria
|
108
|
+
|
109
|
+
### Composites
|
110
|
+
|
111
|
+
|
112
|
+
**MultiMap**
|
113
|
+
* Multiply + Concurrent(Filter or Select, View, Node) + Join
|
114
|
+
|
115
|
+
**Mux** (multiplex)
|
116
|
+
* *AdHoc* to *Stream-Synchronized*
|
117
|
+
* Multiple input streams
|
118
|
+
* Output token for each input token on _any_ input stream
|
119
|
+
|
120
|
+
**Tap**
|
121
|
+
* *Stream-Synchronized*
|
122
|
+
* A Multiply, but defined differently so that it can be injected in another node (?) without changing that node's
|
123
|
+
functionality.
|
124
|
+
* Specify tap-point of other-node when constructing.
|
125
|
+
|
126
|
+
**Runner**
|
127
|
+
* Usually automatically created
|
128
|
+
* Wraps a list of nodes into concurrent. Any inputs given stdin or integer sequences (?) and all outputs are
|
129
|
+
multiplexed to stdout.
|
130
|
+
|
131
|
+
**StateSwitch**
|
132
|
+
* Has a state port and various output streams- state port helps it decide which filter/select branches are chosen
|
133
|
+
(allow either drop or nop?)
|
134
|
+
|
135
|
+
**Split**
|
136
|
+
* Multiply + Concurrent(Filter or Select [with DROP], View, (optional Node))
|
137
|
+
* Like MultiMap except non-matching records are dropped- effectively creating unique streams
|
138
|
+
|
139
|
+
As first token propagates, it's stream-id propagates with it. Nodes that are vertically stateful and therefore need to
|
140
|
+
guarantee that the same stream is feeding them tokens at all time then use it for comparison.
|
141
|
+
|
142
|
+
|
143
|
+
Scratch
|
144
|
+
--------
|
145
|
+
|
146
|
+
Instead of streams / branches maybe just dynamic recognition of which channels have something other than a 1:1
|
147
|
+
input/output token ratio? I.e., which streams are totally synchronous so to speak...
|
148
|
+
|
149
|
+
Lifecycle
|
150
|
+
- if it has non-delayed parameterization, it spawns and sets its parameters
|
151
|
+
- it can be given the output node at creation time, parameterization-time, or any time (even after it has received tokens from
|
152
|
+
at least one inbound stream) before it tries to emit a token on that stream.
|
153
|
+
- internal state machine runs until it needs its first token (if applicable) (or until it's output queue fills up too
|
154
|
+
much)
|
155
|
+
- proceeds to run state machine
|
156
|
+
|
157
|
+
|
158
|
+
|
159
|
+
Compose Classes or Instances (or both)??
|
160
|
+
|
161
|
+
|
162
|
+
Phases
|
163
|
+
1. Kernel-design time:
|
164
|
+
- designate input/output ports/streams
|
165
|
+
2. Design-time:
|
166
|
+
- specify bindings as much as possible
|
167
|
+
- compose
|
168
|
+
- pre-initialize/parameterize as appropriate
|
169
|
+
- specify process network / highest level compositions
|
170
|
+
3. Pre-runtime:
|
171
|
+
- static compliance-check
|
172
|
+
- display process network graph
|
173
|
+
- warnings / errors as appropriate
|
174
|
+
4. Runtime:
|
175
|
+
- dynamic parameterization as appropriate
|
176
|
+
- dynamic running nodes as appropriate
|
177
|
+
- contexts and real stream instances
|
178
|
+
|
179
|
+
|
180
|
+
Specialized (out of band) input/output ports
|
181
|
+
- new output available
|
182
|
+
- new input available (?)
|
183
|
+
- output subscribed by...
|
184
|
+
- input bound by...
|
185
|
+
|
186
|
+
(allows nodes to communicate in an out-of-band fassion... easier to simply specify the peer object in initialize and
|
187
|
+
make sure every node has a general out-of-band communication channel where senders say who they are?)
|
188
|
+
|
189
|
+
* Inputs can be bound to only one output
|
190
|
+
* Outputs can be subscribed to by any number of other nodes
|
191
|
+
* Binding to a node itself assumes the correct input/output if only one of either is available
|
data/doc/example.node
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
node :app, :the_gen >> :a >> :b
|
6
|
+
node :the_gen, 1..1000
|
7
|
+
node :a,
|
8
|
+
|
9
|
+
N[:app] = N[:the_gen] >> N[:a] >> N[:b]
|
10
|
+
|
11
|
+
|
12
|
+
Nodus::Node.new do
|
13
|
+
app = the_gen | a | b | stdout
|
14
|
+
|
15
|
+
the_gen(max=1000) = { 1..max } # Generator because no arity
|
16
|
+
a = {|x| x * 2 }
|
17
|
+
b = {|x| x + 100 }
|
18
|
+
end
|
19
|
+
|
20
|
+
#pseudo_tick(first) = {
|
21
|
+
|
22
|
+
# Basic:
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
# class TheNode < Nodus::Node
|
27
|
+
# def parameterize
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# def process
|
31
|
+
#
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
|
35
|
+
# class Ticker
|
36
|
+
# include Node
|
37
|
+
#
|
38
|
+
# def initialize(symbol)
|
39
|
+
# @symbol = symbol
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# def looped_run
|
43
|
+
# @last_price ||= 60 + rand(30)
|
44
|
+
# @last_price += @rand(-4..4)
|
45
|
+
# emit @last_price
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
|
49
|
+
class Ticker
|
50
|
+
def initialize(symbol)
|
51
|
+
@symbol = symbol
|
52
|
+
end
|
53
|
+
|
54
|
+
def each
|
55
|
+
loop do
|
56
|
+
@last_price ||= 60 + rand(30)
|
57
|
+
@last_price += @rand(-4..4)
|
58
|
+
emit [@last_price, Time.now]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def ticker_for(symb, high, low)
|
65
|
+
Pipe[Ticker.new(symb), Switch[->(x,_){x > high}, ->(x,t){ puts "+++ #{t}: Price above #{high}: #{x}" },
|
66
|
+
->(x,_){x < low }, ->(x,t){ puts "--- #{t}: Price below #{low }: #{x}" }]]
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
|
71
|
+
# node to node transformations
|
72
|
+
# Composition operators NodusClass[...] expected to return another kind of Nodus class.
|
73
|
+
# --- shorthand for currying parameters (including changing the description) via inheritance
|
74
|
+
#
|
75
|
+
# parameters = standardized key/value pairs so specialization works
|
76
|
+
# = always allow lambda/proc so it gets evaluated per incoming token(?) (implies parameterization gets set
|
77
|
+
# more than once... hmmm... maybe a bad idea)
|
78
|
+
#
|
79
|
+
|
80
|
+
|
81
|
+
# P[N[:rand_dist_exp,5],
|
82
|
+
#
|
83
|
+
#
|
84
|
+
# pipe rand_dist_exp(5)
|
85
|
+
#
|
86
|
+
# junction(rand_dist_exp(5),
|
87
|
+
# rand_dist_exp(5),
|
88
|
+
# rand_dist_gaus(0,2))
|
89
|
+
#
|
data/doc/nodes.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
|
2
|
+
# | Node | Input | Output |
|
3
|
+
# | ----------- | ------ | ------ |
|
4
|
+
# | Processor | 1/1 | 1/1 |
|
5
|
+
# | View | 1/1 | 1/1 |
|
6
|
+
# | Generator | 1/0 | 1/1 |
|
7
|
+
# | Branch/Tap | 1/1 | 1/n |
|
8
|
+
# | Recombine | 1/n | 1/1 |
|
9
|
+
|
10
|
+
# | Set | 0/0 | n
|
11
|
+
# | Projection | 1/1 | 1b/1b |
|
12
|
+
# | Sink | 1/1 | 1b/1b |
|
13
|
+
# | Mux | n/n*1 | 1/1 |
|
14
|
+
# | Zip | n/n*1 | 1/1 |
|
15
|
+
# | Switch | 1/1 | n/n*1 |
|
16
|
+
# | StateSwitch | 2/2*1 | n/n*1 |
|
17
|
+
# | Junction | n/m | o/p |
|
18
|
+
|
19
|
+
|
20
|
+
class Node
|
21
|
+
attr_reader :dot_style, :name
|
22
|
+
def initialize(name)
|
23
|
+
@name = name
|
24
|
+
@style_attrs = {}
|
25
|
+
style :shape, :circle
|
26
|
+
style :fontname, 'Helvetica'
|
27
|
+
style :color, '#222222'
|
28
|
+
style :fontcolor, '#444444'
|
29
|
+
end
|
30
|
+
|
31
|
+
def style(k,v)
|
32
|
+
@style_attrs[k] = v.to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class RootNode < Node
|
37
|
+
# (override how it's displayed)
|
38
|
+
# one or more g
|
39
|
+
end
|
40
|
+
|
41
|
+
class StandaloneNode < Node
|
42
|
+
# Starts with a generator
|
43
|
+
end
|
44
|
+
|
45
|
+
class Generator < Node
|
46
|
+
def initialize(*)
|
47
|
+
super
|
48
|
+
style :shape, :trapezium
|
49
|
+
style :orientation, 270
|
50
|
+
style :style, :filled
|
51
|
+
style :fillcolor, '#CCEEDD'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
class Branch < Node
|
57
|
+
def initialize(*)
|
58
|
+
super
|
59
|
+
style :shape, :triangle
|
60
|
+
style :orientation, 90
|
61
|
+
style :style, :filled
|
62
|
+
style :fillcolor, '#EEDDCC'
|
63
|
+
style :label, ''
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Merge < Node
|
68
|
+
def initialize(*)
|
69
|
+
super
|
70
|
+
style :shape, :triangle
|
71
|
+
style :orientation, 270
|
72
|
+
style :style, :filled
|
73
|
+
style :fillcolor, '#EEDDCC'
|
74
|
+
style :label, ''
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|