fast-tcpn 0.0.5

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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +40 -0
  6. data/LICENSE.txt +674 -0
  7. data/README.md +453 -0
  8. data/fast-tcpn.gemspec +40 -0
  9. data/fast-tcpn.rb +192 -0
  10. data/lib/fast-tcpn.rb +25 -0
  11. data/lib/fast-tcpn/clone.rb +7 -0
  12. data/lib/fast-tcpn/clone/using_code_from_stack.rb +50 -0
  13. data/lib/fast-tcpn/clone/using_deep_clone.rb +10 -0
  14. data/lib/fast-tcpn/clone/using_deep_dive.rb +25 -0
  15. data/lib/fast-tcpn/clone/using_marshal.rb +8 -0
  16. data/lib/fast-tcpn/dsl.rb +177 -0
  17. data/lib/fast-tcpn/hash_marking.rb +220 -0
  18. data/lib/fast-tcpn/place.rb +70 -0
  19. data/lib/fast-tcpn/tcpn.rb +288 -0
  20. data/lib/fast-tcpn/timed_hash_marking.rb +87 -0
  21. data/lib/fast-tcpn/timed_place.rb +44 -0
  22. data/lib/fast-tcpn/timed_token.rb +27 -0
  23. data/lib/fast-tcpn/token.rb +30 -0
  24. data/lib/fast-tcpn/transition.rb +224 -0
  25. data/lib/fast-tcpn/version.rb +3 -0
  26. data/spec/callbacks_spec.rb +164 -0
  27. data/spec/dsl/page_spec.rb +195 -0
  28. data/spec/dsl/transition_spec.rb +41 -0
  29. data/spec/hash_marking_spec.rb +9 -0
  30. data/spec/place_spec.rb +10 -0
  31. data/spec/spec_helper.rb +101 -0
  32. data/spec/support/hash_marking_shared.rb +274 -0
  33. data/spec/support/place_shared.rb +66 -0
  34. data/spec/support/token_shared.rb +27 -0
  35. data/spec/support/uses_temp_files.rb +31 -0
  36. data/spec/tcpn_binding_spec.rb +54 -0
  37. data/spec/tcpn_sim_spec.rb +323 -0
  38. data/spec/tcpn_spec.rb +150 -0
  39. data/spec/timed_hash_marking_spec.rb +132 -0
  40. data/spec/timed_place_spec.rb +38 -0
  41. data/spec/timed_token_spec.rb +50 -0
  42. data/spec/token_spec.rb +13 -0
  43. data/spec/transition_spec.rb +236 -0
  44. metadata +156 -0
@@ -0,0 +1,87 @@
1
+ module FastTCPN
2
+
3
+ # This class extends HashMarking to support timed tokens.
4
+ # It is however slower than not-timed version, so use it
5
+ # if you really need it.
6
+ class TimedHashMarking < HashMarking
7
+ InvalidTime = Class.new RuntimeError
8
+
9
+ attr_reader :time, :next_time
10
+
11
+ # Create a new TimedHashMarking
12
+ def initialize(*)
13
+ super
14
+ @time = 0
15
+ @waiting = {}
16
+ # Next time when more tokens will be available from this marking
17
+ @next_time = 0
18
+ end
19
+
20
+ # Creates token with +object+ as its value and adds it to the marking.
21
+ # if no timestamp is given, current time will be used.
22
+ def add(objects, timestamp = @time)
23
+ unless objects.kind_of? Array
24
+ objects = [ objects ]
25
+ end
26
+ objects.each do |object|
27
+ if object.instance_of? Hash
28
+ timestamp = object[:ts] || 0
29
+ object = object[:val]
30
+ end
31
+ token = prepare_token(object, timestamp)
32
+ timestamp = token.timestamp
33
+ if timestamp > @time
34
+ add_to_waiting token
35
+ else
36
+ add_token token
37
+ end
38
+ end
39
+ end
40
+
41
+ # Set current time for the marking.
42
+ # This will cause moving tokens from waiting to active list.
43
+ # Putting clock back will cause error.
44
+ def time=(time)
45
+ if time < @time
46
+ raise InvalidTime.new("You are trying to put back clock from #{@time} back to #{time}")
47
+ end
48
+ @time = time
49
+ @waiting.keys.sort.each do |timestamp|
50
+ if timestamp > @time
51
+ @next_time = timestamp
52
+ break
53
+ end
54
+ @waiting[timestamp].each { |token| add_token token }
55
+ @waiting.delete timestamp
56
+ end
57
+ @next_time = 0 if @waiting.empty?
58
+ @time
59
+ end
60
+
61
+ def token_type
62
+ TimedToken
63
+ end
64
+
65
+ private
66
+
67
+ def prepare_token(object, timestamp = 0)
68
+ if object.instance_of? token_type
69
+ clone object
70
+ elsif object.kind_of? token_type
71
+ token_type.new clone(object.value), object.timestamp
72
+ else
73
+ token_type.new clone(object), timestamp
74
+ end
75
+ end
76
+
77
+ def add_to_waiting(token)
78
+ @waiting[token.timestamp] ||= []
79
+ @waiting[token.timestamp] << token
80
+ if @next_time == 0 || token.timestamp < @next_time
81
+ @next_time = token.timestamp
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,44 @@
1
+ module FastTCPN
2
+ # This class extends Place class to support time and timed tokens.
3
+ # TimedHashMarking class is used to store tokens.
4
+ #
5
+ # It is however slower in simulation than not-timed version, so
6
+ # use with care when you really need it.
7
+ class TimedPlace < Place
8
+ # Create new timed place.
9
+ # +name+ is unique identifier of the place
10
+ # +keys+ are used to efficiently access marking in this place
11
+ # +net+ is TCPN class object, used to enable callbacks
12
+ def initialize(name, keys = {}, net = nil)
13
+ super
14
+ @marking = TimedHashMarking.new keys
15
+ @clock = 0
16
+ end
17
+
18
+ # returns next timestamp that will cause more tokens
19
+ # to be available in this place
20
+ def next_time
21
+ @marking.next_time
22
+ end
23
+
24
+ # set current time for this place
25
+ # (will move tokens from waiting to active state).
26
+ # Putting clock back will cause error.
27
+ def time=(val)
28
+ @clock = val
29
+ @marking.time = val
30
+ end
31
+
32
+ # Adds token with specified timestamp to the place.
33
+ # Any callbacks defined for places will be fired.
34
+ def add(token, timestamp = nil)
35
+ @net.call_callbacks(:place, :add, Event.new(@name, [token], @net)) unless @net.nil?
36
+ if timestamp.nil?
37
+ @marking.add token
38
+ else
39
+ @marking.add token, timestamp
40
+ end
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,27 @@
1
+ module FastTCPN
2
+
3
+ # Extends Token class to support time.
4
+ # Stores also timestamp.
5
+ class TimedToken < Token
6
+
7
+ attr_accessor :timestamp
8
+
9
+ # Create new token with specified +value+ and +timestamp+.
10
+ def initialize(value, timestamp = 0)
11
+ super value
12
+ @timestamp = timestamp
13
+ end
14
+
15
+ # Create new token with the same value but new timestamp
16
+ # Use it to quickly delay tokens while firing a transition
17
+ # if token value change is not required.
18
+ def with_timestamp(timestamp)
19
+ self.class.new(value, timestamp)
20
+ end
21
+
22
+ def to_hash
23
+ { val: value, ts: timestamp }
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ module FastTCPN
2
+
3
+ # Represents tokens in CPN.
4
+ # Stores values, ensures correct handling of
5
+ # equality and hash values.
6
+ class Token
7
+ alias orig_hash hash
8
+ attr_reader :value, :hash, :token_id
9
+
10
+ def initialize(value)
11
+ @value = value
12
+ @token_id = object_id
13
+ @hash = orig_hash
14
+ end
15
+
16
+ def eql?(o)
17
+ @token_id == o.token_id
18
+ rescue # faster then checking for instance_of?
19
+ false
20
+ end
21
+
22
+ def to_hash
23
+ { val: value }
24
+ end
25
+
26
+ alias == eql?
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,224 @@
1
+ module FastTCPN
2
+
3
+ class OutputArc
4
+ attr_reader :place, :block
5
+ # the +block+ will be given actual binding and
6
+ # must return token that should be put in the
7
+ # +palce+
8
+ def initialize(place, block)
9
+ @place, @block = place, block
10
+ end
11
+ end
12
+
13
+ class TCPNBinding
14
+ TokenNotFound = Class.new RuntimeError
15
+
16
+ def initialize(mapping, marking_for)
17
+ @mapping, @marking_for = mapping, marking_for
18
+ end
19
+
20
+ def [](place_name)
21
+ token = @mapping[place_name]
22
+ if token.nil?
23
+ raise TokenNotFound.new("No mapping for place `#{place_name}`")
24
+ end
25
+ marking = @marking_for[place_name]
26
+ if marking.nil?
27
+ raise TokenNotFound.new("No marking for place `#{place_name}`")
28
+ end
29
+ if token.instance_of? Array
30
+ token.map { |t| get_new_token marking, t }
31
+ else
32
+ get_new_token marking, token
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def get_new_token(marking, token)
39
+ new_token = marking.get token
40
+ if new_token.nil?
41
+ raise TokenNotFound.new("There was no `#{token.inspect}` in `#{marking.inspect}`!")
42
+ end
43
+ new_token
44
+ end
45
+ end
46
+
47
+ # This is implementation of TCPN transition.
48
+ # It has input and output places and it can be fired.
49
+ class Transition
50
+ InvalidToken = Class.new RuntimeError
51
+
52
+ class FiringError < RuntimeError
53
+ attr_reader :cause, :transition
54
+
55
+ def initialize(transition, cause)
56
+ @transition, @cause = transition, cause
57
+ set_backtrace @cause.backtrace
58
+ end
59
+
60
+ def inspect
61
+ "<#{self.class} #{@cause.inspect} in transition `#{@transition.name}`>"
62
+ end
63
+
64
+ def to_s
65
+ inspect
66
+ end
67
+ end
68
+
69
+ attr_reader :name
70
+
71
+ # Class passed to callback fired when a transition is fired.
72
+ # Describes details of the event that caused the callback to be fired.
73
+ Event = Struct.new(:transition, :binding, :clock, :tcpn)
74
+
75
+ # Create new Transition.
76
+ # +name+ identifies this transition
77
+ # +net+ is reference to TCPN object, required to enable callback handling.
78
+ def initialize(name, net = nil)
79
+ @name = name
80
+ @inputs = []
81
+ @outputs = []
82
+ @sentry = nil
83
+ @net = net
84
+ end
85
+
86
+ # Add input arc from the +place+.
87
+ # No inscription currently possible,
88
+ # one token will be taken from the +place+
89
+ # each time the transition is fired.
90
+ def input(place)
91
+ raise "This is not a Place object!" unless place.kind_of? Place
92
+ @inputs << place
93
+ end
94
+
95
+ # Add output arc to the +place+.
96
+ # +block+ is the arc's expresstion, it will be called while firing
97
+ # transition. Value returned from the block will be put in output
98
+ # place. The block gets +binding+, and +clock+ values. +binding+ is
99
+ # a hash with names of input places as keys nad tokens as values.
100
+ def output(place, &block)
101
+ raise "This is not a Place object!" unless place.kind_of? Place
102
+ raise "Tried to define output arc without expression! Block is required!" unless block_given?
103
+ @outputs << OutputArc.new(place, block)
104
+ end
105
+
106
+ # Define sentry for this transition as a block.
107
+ # The block gets three parameters: +marking_for+, +clock+ and +result+.
108
+ # +marking_for+ is a hash with input place names as keys and marking objects
109
+ # as values. Thus one can iterate over tokens from specified input places.
110
+ # This block is supposed to push a hash of valid binding to the result like this:
111
+ # result << { output_place_name1 => token, output_place_name2 => token2 }
112
+ def sentry(&block)
113
+ @sentry = block
114
+ end
115
+
116
+ # fire this transition if possible
117
+ # returns true if fired false otherwise
118
+ def fire(clock = 0)
119
+
120
+ # Marking is shuffled each time before it is
121
+ # used so here we can take first found binding
122
+ mapping = Enumerator.new do |y|
123
+ get_sentry.call(input_markings, clock, y)
124
+ end.first
125
+
126
+ return false if mapping.nil?
127
+
128
+ tcpn_binding = TCPNBinding.new mapping, input_markings
129
+
130
+ call_callbacks :before, Event.new(@name, tcpn_binding, clock, @net)
131
+
132
+ tokens_for_outputs = @outputs.map do |o|
133
+ o.block.call(tcpn_binding, clock)
134
+ end
135
+
136
+ mapping.each do |place_name, token|
137
+ unless token.kind_of? Token
138
+ t = if token.instance_of? Array
139
+ token
140
+ else
141
+ [ token ]
142
+ end
143
+ t.each do |t|
144
+ unless t.kind_of? Token
145
+ raise InvalidToken.new("#{t.inspect} put by sentry for transition `#{name}` in binding for `#{place_name}`")
146
+ end
147
+ end
148
+ end
149
+ deleted = find_input(place_name).delete(token)
150
+ if deleted.nil?
151
+ raise InvalidToken.new("#{token.inspect} put by sentry for transition `#{name}` does not exists in `#{place_name}`")
152
+ end
153
+ end
154
+
155
+ @outputs.each do |o|
156
+ token = tokens_for_outputs.shift
157
+ o.place.add token unless token.nil?
158
+ end
159
+
160
+ call_callbacks :after, Event.new(@name, mapping, clock, @net)
161
+
162
+ true
163
+ rescue InvalidToken
164
+ raise
165
+ rescue RuntimeError => e
166
+ raise FiringError.new(self, e)
167
+ end
168
+
169
+ # Returns true if no custom sentry was defined for this transition.
170
+ def default_sentry?
171
+ @sentry.nil?
172
+ end
173
+
174
+ # Number of input places
175
+ def inputs_size
176
+ @inputs.size
177
+ end
178
+
179
+ # Number of output places
180
+ def outputs_size
181
+ @outputs.size
182
+ end
183
+
184
+ private
185
+
186
+ def get_sentry
187
+ @sentry || default_sentry
188
+ end
189
+
190
+ def call_callbacks(event, event_object)
191
+ return if @net.nil?
192
+ @net.call_callbacks(:transition, event, event_object)
193
+ end
194
+
195
+ def input_markings
196
+ bnd = {}
197
+ @inputs.each do |place|
198
+ bnd[place.name] = place.marking
199
+ end
200
+ bnd
201
+ end
202
+
203
+ def find_input(name)
204
+ @inputs.each do |place|
205
+ return place if place.name == name
206
+ end
207
+ nil
208
+ end
209
+
210
+ def default_sentry
211
+ proc do |marking_for, clock, result|
212
+ mapping = marking_for.map do |place, marking|
213
+ break nil if marking.first.nil?
214
+ { place => marking.first }
215
+ end
216
+ unless mapping.nil?
217
+ result << mapping.compact.reduce(:merge)
218
+ end
219
+ end
220
+ end
221
+ end
222
+
223
+ end
224
+
@@ -0,0 +1,3 @@
1
+ module FastTCPN
2
+ VERSION = '0.0.5'
3
+ end
@@ -0,0 +1,164 @@
1
+ require 'spec_helper'
2
+
3
+ describe FastTCPN::TCPN do
4
+ describe "callback" do
5
+ let :tcpn do
6
+ m = FastTCPN::TCPN.new
7
+ p1 = m.timed_place 'input'
8
+ p2 = m.place 'output'
9
+ t = m.transition 'send'
10
+ t.input p1
11
+ t.output p2 do |binding, clock|
12
+ binding['input']
13
+ end
14
+ p1.add :data_package, 100
15
+ m
16
+ end
17
+
18
+ describe "for transition" do
19
+ shared_examples "calls callbacks" do
20
+ it "is called when transition is fired" do
21
+ count = 0
22
+ tcpn.cb_for :transition, tag do |tag, event|
23
+ expect(event.binding['input'].value).to eq :data_package
24
+ expect(event.transition).to eq 'send'
25
+ expect(event.clock).to eq 100
26
+ count += 1
27
+ end
28
+ tcpn.sim
29
+ expect(count).to eq expected_count
30
+ end
31
+ end
32
+ describe "without tag" do
33
+ let(:expected_count) { 2 }
34
+ let(:tag) { }
35
+ include_examples "calls callbacks"
36
+ end
37
+
38
+ describe "with :before tag" do
39
+ let(:expected_count) { 1 }
40
+ let(:tag) { :before }
41
+ include_examples "calls callbacks"
42
+ end
43
+
44
+ describe "with :after tag" do
45
+ let(:expected_count) { 1 }
46
+ let(:tag) { :after }
47
+ include_examples "calls callbacks"
48
+ end
49
+
50
+ end
51
+
52
+ describe "for place" do
53
+ it "is called when token is removed" do
54
+ called = 0
55
+ tcpn.cb_for :place, :remove do |tag, event|
56
+ expect(tag).to eq :remove
57
+ expect(event.place).to eq 'input'
58
+ expect(event.clock).to eq 100
59
+ expect(event.tokens.first.value).to eq :data_package
60
+ called += 1
61
+ end
62
+ tcpn.sim
63
+ expect(called).to eq 1
64
+ end
65
+
66
+ it "is called when token is added" do
67
+ called = 0
68
+ tcpn.cb_for :place, :add do |tag, event|
69
+ expect(tag).to eq :add
70
+ expect(event.place).to eq 'output'
71
+ expect(event.clock).to eq nil # this place is not timed!
72
+ called += 1
73
+ end
74
+ tcpn.sim
75
+ expect(called).to eq 1
76
+ end
77
+
78
+ context "when token Array is added or removed" do
79
+ let :tcpn do
80
+ m = FastTCPN::TCPN.new
81
+ p1 = m.timed_place 'input'
82
+ p2 = m.place 'output'
83
+ t = m.transition 'send'
84
+ t.input p1
85
+ t.output p2 do |binding, clock|
86
+ binding['input']
87
+ end
88
+ t.sentry do |marking_for, clock, result|
89
+ unless marking_for[p1.name].empty?
90
+ tokens = marking_for[p1.name].to_a
91
+ result << { p1.name => tokens }
92
+ end
93
+ end
94
+ p1.add :data_package1, 100
95
+ p1.add :data_package2, 100
96
+ m
97
+ end
98
+
99
+ it "passes token array when it is added" do
100
+ called = 0
101
+ tcpn.cb_for :place, :add do |tag, event|
102
+ expect(tag).to eq :add
103
+ expect(event.place).to eq 'output'
104
+ expect(event.tokens.map{ |t| t.value }).to match_array [ :data_package1, :data_package2 ]
105
+ called += 1
106
+ end
107
+ tcpn.sim
108
+ expect(called).to eq 1
109
+ end
110
+ it "passes token array when it is removed" do
111
+ called = 0
112
+ tcpn.cb_for :place, :remove do |tag, event|
113
+ expect(tag).to eq :remove
114
+ expect(event.place).to eq 'input'
115
+ expect(event.clock).to eq 100
116
+ expect(event.tokens.map{ |t| t.value }).to match_array [ :data_package1, :data_package2 ]
117
+ called += 1
118
+ end
119
+ tcpn.sim
120
+ expect(called).to eq 1
121
+ end
122
+ end
123
+ end
124
+
125
+ describe "for clock" do
126
+
127
+ let :tcpn do
128
+ m = FastTCPN::TCPN.new
129
+ p1 = m.timed_place 'input'
130
+ p2 = m.place 'output'
131
+ t = m.transition 'send'
132
+ t.input p1
133
+ t.output p2 do |binding, clock|
134
+ binding['input']
135
+ end
136
+ p1.add :data_package, 100
137
+ m
138
+ end
139
+
140
+ it "is called before clock is changed" do
141
+ called = 0
142
+ tcpn.cb_for :clock, :before do |tag, event|
143
+ expect(tag).to eq :before
144
+ expect(event.clock).to eq(0) if called == 0
145
+ called += 1
146
+ end
147
+ tcpn.sim
148
+ expect(called).to be > 1
149
+ end
150
+
151
+ it "is called after clock is changed" do
152
+ called = 0
153
+ tcpn.cb_for :clock, :after do |tag, event|
154
+ expect(tag).to eq :after
155
+ expect(event.clock).to eq 100
156
+ expect(event.previous_clock).to eq 0
157
+ called += 1
158
+ end
159
+ tcpn.sim
160
+ expect(called).to eq 1
161
+ end
162
+ end
163
+ end
164
+ end