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,220 @@
1
+ class Array
2
+
3
+ # This will slow down with time -- the more
4
+ # times we yield, the harder will be to hit
5
+ # an unused value. So use standerd shuffle
6
+ # if you need significant amount of values
7
+ # shuffled. But if you need just a few, this
8
+ # one is faster ;-)
9
+ def lazy_shuffle
10
+ return enum_for(:lazy_shuffle) unless block_given?
11
+ was = {}
12
+ self.size.times do
13
+ i = 0
14
+ begin
15
+ i = rand self.size
16
+ end while was.has_key?(i)
17
+ was[i] = true
18
+ yield self[i]
19
+ end
20
+ end
21
+ end
22
+
23
+ module FastTCPN
24
+
25
+ class HashMarking
26
+
27
+ include Clone
28
+
29
+
30
+ InvalidToken = Class.new RuntimeError
31
+ InvalidKey = Class.new RuntimeError
32
+ CannotAddKeys = Class.new RuntimeError
33
+
34
+ include Enumerable
35
+
36
+ attr_reader :keys
37
+
38
+ # Creates new HashMarking with specified keys. At least
39
+ # one key must be specified. The keys are used to
40
+ # store tokens in Hashes -- one hash for each key. Thus
41
+ # finding tokens by the keys is fast.
42
+ #
43
+ # +keys+ is a hash of the form: { key_name => method }, where +key_name+ is a name that
44
+ # will be used to access tokens indexed by this key and +method+ is a method that should
45
+ # be called on token's value to get value that should be used group tokens indexed by this key.
46
+ def initialize(keys = {})
47
+ @keys = keys
48
+ @lists = {}
49
+ @global_list = {}
50
+ end
51
+
52
+ # Allows to iterate over all values in marking or over all values for which
53
+ # specified +key+ has specified +value+. If no block is given, returns adequate
54
+ # Enumerator. Yielded values are deep-cloned so you can use them without fear of
55
+ # interfering with TCPN simulation.
56
+ #
57
+ # Values are yielded in random order -- each time each is called with block
58
+ # or a new Enumerator is created, the order is changed. Thus tokens are selection
59
+ # is `in some sense fair`. Current implementation assumes, that in most cases iteration
60
+ # will finish quickly, without yielding large number of tokens. In such cases the
61
+ # shuffling algorithm is efficient. But if for some reason most tokens from marking
62
+ # should be yielded, it will be less and less efficient, with every nxt token. In this
63
+ # case one should consider using standard +shuffle+ method here instead of +lazy_shuffle+.
64
+ def each(key = nil, value = nil)
65
+ return enum_for(:each, key, value) unless block_given?
66
+ return if empty?
67
+ list_for(key, value).lazy_shuffle do |token|
68
+ yield clone token
69
+ end
70
+ end
71
+
72
+ # Add new keys to this marking. This list will be merged with exisiting keys.
73
+ # Adding keys is possible only if marking is empty, otherwise CannotAddKeys
74
+ # exception will be raised.
75
+ def add_keys(keys)
76
+ unless empty?
77
+ raise CannotAddKeys.new("marking not empty!");
78
+ end
79
+ @keys.merge!(keys)
80
+ end
81
+
82
+ # True if the marking is empty.
83
+ def empty?
84
+ size == 0
85
+ end
86
+
87
+ # Creates new token of the +object+ and adds it to the marking.
88
+ # Objects added to the marking are deep-cloned, so you can use them
89
+ # without fear to interfere with TCPN simulation. But have it in mind!
90
+ # If you put a large object with a lot of references in the marking,
91
+ # it will significanntly slow down simulation and increase memory usage.
92
+ def add(objects)
93
+ unless objects.kind_of? Array
94
+ objects = [ objects ]
95
+ end
96
+ objects.each do |object|
97
+ value = object
98
+ if object.instance_of? Hash
99
+ value = object[:val]
100
+ end
101
+ add_token prepare_token(value)
102
+ end
103
+ end
104
+
105
+ alias << add
106
+
107
+ # Deletes the +token+ from the marking.
108
+ # To do it you must first find the token in
109
+ # the marking.
110
+ def delete(tokens)
111
+ unless tokens.instance_of? Array
112
+ tokens = [ tokens ]
113
+ end
114
+ removed = tokens.map do |token|
115
+ validate_token!(token)
116
+ delete_token(token)
117
+ end
118
+ if removed.size == 1
119
+ removed.first
120
+ else
121
+ removed
122
+ end
123
+ end
124
+
125
+ # Returns number of tokens in this marking
126
+ def size
127
+ @global_list.size
128
+ end
129
+
130
+ # :nodoc:
131
+ # Return fresh, unaltered clone of given token
132
+ # The given token's value could have been changed when it was
133
+ # passed to a user-defined code. Use this method to refresh this value.
134
+ # Will work as long as user interfered with token value, but not with
135
+ # the token object iself.
136
+ #
137
+ # For internal use, while firing transition
138
+ def get(token)
139
+ clone @global_list[token]
140
+ end
141
+
142
+ # reimplement if inherited classes need different one
143
+ def token_type
144
+ Token
145
+ end
146
+
147
+ private
148
+
149
+ def tokens_by_key(key_name, value)
150
+ unless @keys.has_key? key_name
151
+ raise InvalidKey.new(key_name)
152
+ end
153
+ @lists[key_name][value]
154
+ end
155
+
156
+ def each_key_with(token)
157
+ @keys.each do |name, method_with_params|
158
+ params = nil
159
+ method = method_with_params
160
+ if method_with_params.instance_of? Array
161
+ method = method_with_params.first
162
+ params = method_with_params[1..-1]
163
+ end
164
+ unless token.value.respond_to? method
165
+ raise InvalidToken.new("#{token.inspect} does not respond to #{method.inspect}")
166
+ end
167
+ value = token.value.send(method, *params)
168
+ yield name, value
169
+ end
170
+ end
171
+
172
+ def prepare_token(object)
173
+ if object.instance_of? token_type
174
+ clone object
175
+ elsif object.kind_of? token_type
176
+ token_type.new clone(object.value)
177
+ else
178
+ token_type.new clone(object)
179
+ end
180
+ end
181
+
182
+ def add_token(token)
183
+ @global_list[token] = token
184
+ each_key_with(token) do |key_name, value|
185
+ @lists[key_name] ||= {}
186
+ @lists[key_name][value] ||= []
187
+ @lists[key_name][value] << token
188
+ end
189
+ end
190
+
191
+ def validate_token!(token)
192
+ unless token.instance_of? token_type
193
+ raise InvalidToken.new "#{token} is not a #{token_type} object!"
194
+ end
195
+ end
196
+
197
+ def delete_token(token)
198
+ return nil unless @global_list.delete token
199
+ each_key_with(token) do |key_name, value|
200
+ next unless @lists.has_key? key_name
201
+ next unless @lists[key_name].has_key? value
202
+ @lists[key_name][value].delete token
203
+ end
204
+ token
205
+ end
206
+
207
+ def list_for(key, value)
208
+ list = if !key.nil? && !value.nil?
209
+ tokens_by_key key, value
210
+ else
211
+ @global_list.keys
212
+ end
213
+ if list.nil?
214
+ list = []
215
+ end
216
+ list
217
+ end
218
+ end
219
+
220
+ end
@@ -0,0 +1,70 @@
1
+ module FastTCPN
2
+
3
+ # This class imlements standard place of CPN model.
4
+ # Marking reprezented as HashMarking object with
5
+ # specified keys (see #new and #add_keys)
6
+ #
7
+ # It supports neither time nor timed tokens!
8
+ class Place
9
+
10
+ # Class passed to callback fired when tokens are added and/or removed from places.
11
+ # Describes details of the event that caused the callback to be fired.
12
+ Event = Struct.new(:place, :tokens, :clock, :tcpn) do
13
+ def initialize(*)
14
+ super
15
+ unless self.tokens.instance_of? Array
16
+ self.tokens = [ self.tokens ]
17
+ end
18
+ end
19
+ end
20
+
21
+ attr_reader :name
22
+
23
+ # Create new Place object.
24
+ # +name+ is unique identifier of this place
25
+ # +keys+ are token keys used to quickly find tokens in marking (see HashMarking)
26
+ # +net+ is reference to TCPN object, required to support callbacks
27
+ def initialize(name, keys = {}, net = nil)
28
+ @name = name
29
+ @marking = HashMarking.new keys
30
+ @net = net
31
+ end
32
+
33
+ # Return reference this place's marking.
34
+ # If you modify marking using this reference, no
35
+ # callbacks defined for places will be fired!
36
+ def marking
37
+ @marking
38
+ end
39
+
40
+ # Removes +token+ from this place
41
+ # Callbacks defined for places will be fired
42
+ def delete(token)
43
+ @net.call_callbacks(:place, :remove, Event.new(@name, token, @clock, @net)) unless @net.nil?
44
+ @marking.delete token
45
+ end
46
+
47
+ # Add +token+ to this place
48
+ # Callbacks defined for places will be fired
49
+ def add(token)
50
+ @net.call_callbacks(:place, :add, Event.new(@name, token, @clock, @net)) unless @net.nil?
51
+ @marking.add token
52
+ end
53
+
54
+ # Add keys that will be used to store and quickly find tokens in this place's marking.
55
+ # see HashMarking for more info.
56
+ def add_keys(keys)
57
+ @marking.add_keys keys
58
+ end
59
+
60
+ # returns keys used to store and access tokens in this place.
61
+ def keys
62
+ @marking.keys
63
+ end
64
+
65
+ def inspect
66
+ "<#{self.class} name: #{name.inspect}>"
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,288 @@
1
+ module FastTCPN
2
+
3
+ # This class represents TCPN model: places, transitions, arcs
4
+ class TCPN
5
+
6
+ PlaceTypeDoesNotMach = Class.new RuntimeError
7
+ InvalidCallback = Class.new RuntimeError
8
+ StopSimulation = Class.new RuntimeError
9
+
10
+ class SimulationError < RuntimeError
11
+ def initialize(cause)
12
+ @cause = cause
13
+ set_backtrace @cause.backtrace
14
+ set_dir
15
+ end
16
+
17
+ alias full_backtrace backtrace
18
+
19
+ def backtrace
20
+ return full_backtrace if @dir.nil?
21
+ return full_backtrace if FastTCPN.debug
22
+ full_backtrace.select { |b| b !~ /#{@dir}/ }
23
+ end
24
+
25
+ def inspect
26
+ "<#{self.class} #{@cause.inspect}>"
27
+ end
28
+
29
+ def message
30
+ "#{self.class}: #{@cause.message}"
31
+ end
32
+
33
+ private
34
+ def set_dir
35
+ @dir = __FILE__
36
+ @dir = File.dirname(@dir) while(File.basename(@dir) != 'lib' && !@dir.empty? && @dir != '/')
37
+ @dir = nil if @dir.empty? || @dir == '/'
38
+ end
39
+ end
40
+
41
+ ClockEvent = Struct.new(:clock, :previous_clock, :tcpn)
42
+
43
+ class Clock
44
+ # :nodoc:
45
+
46
+ def initialize
47
+ @value = 0
48
+ end
49
+
50
+ def set(value)
51
+ return false if value <= @value
52
+ @value = value
53
+ true
54
+ end
55
+
56
+ def get
57
+ @value
58
+ end
59
+
60
+ end
61
+
62
+ def initialize
63
+ @places = {}
64
+ @timed_places = {}
65
+ @transitions = []
66
+ @clock = Clock.new
67
+ @callbacks = {
68
+ transition: { before: [], after: [] },
69
+ place: { add: [], remove: [] },
70
+ clock: { before: [], after: [] }
71
+ }
72
+ @stopped = false
73
+ end
74
+
75
+ # Create and return new not timed place for this model.
76
+ #
77
+ # If a place with this
78
+ # name exists somewhere in the model (e.g. on other pages), and object
79
+ # representing exisiting place will be returned. Keys of both keys will
80
+ # be merged. For description of keys see doc for HashMarking.
81
+ def place(name, keys = {})
82
+ create_or_find_place(name, keys, Place)
83
+ end
84
+
85
+ # Create and return a new timed place for this model.
86
+ #
87
+ # If a place with this
88
+ # name exists somewhere in the model (e.g. on other pages), and object
89
+ # representing exisiting place will be returned. Keys of both keys will
90
+ # be merged. For description of keys see doc for HashMarking.
91
+ def timed_place(name, keys = {})
92
+ place = create_or_find_place(name, keys, TimedPlace)
93
+ @timed_places[place] = true
94
+ place
95
+ end
96
+
97
+ # Create and return new transition for this model.
98
+ # +name+ identifies transition in the net.
99
+ def transition(name)
100
+ t = find_transition name
101
+ if t.nil?
102
+ t = Transition.new name, self
103
+ @transitions << t
104
+ end
105
+ t
106
+ end
107
+
108
+ # Returns place with given name for this net.
109
+ def find_place(name)
110
+ @places[name]
111
+ end
112
+
113
+ # Returns transition with given name for this net.
114
+ def find_transition(name)
115
+ @transitions.select { |t| t.name == name }.first
116
+ end
117
+
118
+ # Number of places in this net.
119
+ def places_count
120
+ @places.size
121
+ end
122
+
123
+ # Number of transitions in this net.
124
+ def transitions_count
125
+ @transitions.size
126
+ end
127
+
128
+ # Starts simulation of this net.
129
+ def sim
130
+ @stopped = catch :stop_simulation do
131
+ begin
132
+ fired = fire_transitions
133
+ advanced = move_clock_to find_next_time
134
+ end while fired || advanced
135
+ end
136
+ @stopped = false if @stopped == nil
137
+ rescue StandardError => e
138
+ raise SimulationError.new(e)
139
+ end
140
+
141
+ alias run sim
142
+
143
+ # Returns current value of global simulation clock for this net.
144
+ def clock
145
+ @clock.get
146
+ end
147
+
148
+ # Defines new callback for this net.
149
+ # +what+ can be +:transition+, +:place+ or +:clock+.
150
+ # Transition callbacks are fired when transitions are fired, place
151
+ # callbacks when place marking changes, clock callbacks when clock is moved.
152
+ # +tag+ for transition and clock callback can be +:before+ or +:after+, for
153
+ # place, can be +:add+ or +:remove+. It defines when the callbacks fill be
154
+ # fired. If omitted, it will be called for both cases.
155
+ #
156
+ # Callback block for transition gets value of event +tag+ (:before or :after)
157
+ # FastTCPN::Transition::Event object.
158
+ #
159
+ # Callback block for place gets value of event +tag+ (:add or :remove)
160
+ # and FastTCPN::Place::Event object.
161
+ #
162
+ # Callback block for clock gets value of event +tag+ (:before or :after)
163
+ # and FastTCPN::TCPN::ClockEvent object.
164
+ def cb_for(what, tag = nil, &block)
165
+ if what == :transition
166
+ cb_for_transition tag, &block
167
+ elsif what == :place
168
+ cb_for_place tag, &block
169
+ elsif what == :clock
170
+ cb_for_clock tag, &block
171
+ else
172
+ raise InvalidCallback.new "Don't know how to add callback for #{what}"
173
+ end
174
+ end
175
+
176
+ # :nodoc:
177
+ # Calls callbacks, for internal use.
178
+ def call_callbacks(what, tag, *params)
179
+ @callbacks[what][tag].each do |block|
180
+ block.call tag, *params
181
+ end
182
+ end
183
+
184
+
185
+ # OLD API, derived from tcpn gem
186
+
187
+ # Return marking for specified place in a form of Hash:
188
+ # { val: token_value, ts: token_timestamp }
189
+ def marking_for(name)
190
+ find_place(name).marking.map { |t| t.to_hash }
191
+ end
192
+
193
+ def add_marking_for(name, m)
194
+ token = m
195
+ find_place(name).add token
196
+ end
197
+
198
+ # stop simulation now, no matter if there are any enabled transitions
199
+ def stop
200
+ # raise StopSimulation.new
201
+ throw :stop_simulation, true
202
+ end
203
+
204
+ # True if simulation was stopped using #stop method
205
+ def stopped?
206
+ @stopped
207
+ end
208
+
209
+ private
210
+
211
+ def cb_for_transition(tag, &block)
212
+ if tag == :before || tag.nil?
213
+ @callbacks[:transition][:before] << block
214
+ end
215
+ if tag == :after || tag.nil?
216
+ @callbacks[:transition][:after] << block
217
+ end
218
+ end
219
+
220
+ def cb_for_place(tag, &block)
221
+ if tag == :add || tag.nil?
222
+ @callbacks[:place][:add] << block
223
+ end
224
+ if tag == :remove || tag.nil?
225
+ @callbacks[:place][:remove] << block
226
+ end
227
+ end
228
+
229
+ def cb_for_clock(tag, &block)
230
+ if tag == :before || tag.nil?
231
+ @callbacks[:clock][:before] << block
232
+ end
233
+ if tag == :after || tag.nil?
234
+ @callbacks[:clock][:after] << block
235
+ end
236
+ end
237
+
238
+ def create_or_find_place(name, keys, type)
239
+ place = @places[name]
240
+ if place.nil?
241
+ place = type.new name, keys, self
242
+ else
243
+ unless type == place.class
244
+ raise PlaceTypeDoesNotMatch.new "You tried to create place #{name} of type #{type}, but it already exsists and has type #{place.class}"
245
+ end
246
+ place.add_keys keys
247
+ end
248
+ @places[name] = place
249
+ end
250
+
251
+ def move_clock_to(val)
252
+ previous_clock = @clock.get
253
+ call_callbacks(:clock, :before, ClockEvent.new(@clock.get, nil, @tcpn))
254
+ return false unless @clock.set val
255
+ @timed_places.each_key do |place|
256
+ place.time = val
257
+ end
258
+ call_callbacks(:clock, :after, ClockEvent.new(@clock.get, previous_clock, @tcpn))
259
+ true
260
+ end
261
+
262
+ def fire_transitions
263
+ fired_count = 0
264
+ begin
265
+ fired = false
266
+ @transitions.shuffle.each do |transition|
267
+ if transition.fire clock
268
+ fired_count += 1
269
+ fired = true
270
+ end
271
+ end
272
+ end while fired
273
+ fired_count > 0
274
+ end
275
+
276
+ def find_next_time
277
+ time = 0
278
+ @timed_places.each_key do |place|
279
+ next_time = place.next_time
280
+ if next_time > clock && (time == 0 || next_time < time)
281
+ time = next_time
282
+ end
283
+ end
284
+ time
285
+ end
286
+
287
+ end
288
+ end