fast-tcpn 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +40 -0
- data/LICENSE.txt +674 -0
- data/README.md +453 -0
- data/fast-tcpn.gemspec +40 -0
- data/fast-tcpn.rb +192 -0
- data/lib/fast-tcpn.rb +25 -0
- data/lib/fast-tcpn/clone.rb +7 -0
- data/lib/fast-tcpn/clone/using_code_from_stack.rb +50 -0
- data/lib/fast-tcpn/clone/using_deep_clone.rb +10 -0
- data/lib/fast-tcpn/clone/using_deep_dive.rb +25 -0
- data/lib/fast-tcpn/clone/using_marshal.rb +8 -0
- data/lib/fast-tcpn/dsl.rb +177 -0
- data/lib/fast-tcpn/hash_marking.rb +220 -0
- data/lib/fast-tcpn/place.rb +70 -0
- data/lib/fast-tcpn/tcpn.rb +288 -0
- data/lib/fast-tcpn/timed_hash_marking.rb +87 -0
- data/lib/fast-tcpn/timed_place.rb +44 -0
- data/lib/fast-tcpn/timed_token.rb +27 -0
- data/lib/fast-tcpn/token.rb +30 -0
- data/lib/fast-tcpn/transition.rb +224 -0
- data/lib/fast-tcpn/version.rb +3 -0
- data/spec/callbacks_spec.rb +164 -0
- data/spec/dsl/page_spec.rb +195 -0
- data/spec/dsl/transition_spec.rb +41 -0
- data/spec/hash_marking_spec.rb +9 -0
- data/spec/place_spec.rb +10 -0
- data/spec/spec_helper.rb +101 -0
- data/spec/support/hash_marking_shared.rb +274 -0
- data/spec/support/place_shared.rb +66 -0
- data/spec/support/token_shared.rb +27 -0
- data/spec/support/uses_temp_files.rb +31 -0
- data/spec/tcpn_binding_spec.rb +54 -0
- data/spec/tcpn_sim_spec.rb +323 -0
- data/spec/tcpn_spec.rb +150 -0
- data/spec/timed_hash_marking_spec.rb +132 -0
- data/spec/timed_place_spec.rb +38 -0
- data/spec/timed_token_spec.rb +50 -0
- data/spec/token_spec.rb +13 -0
- data/spec/transition_spec.rb +236 -0
- metadata +156 -0
data/README.md
ADDED
@@ -0,0 +1,453 @@
|
|
1
|
+
FastTCPN (fast-tcpn) library technical documentation
|
2
|
+
========
|
3
|
+
|
4
|
+
Fast simulation tool for Timed Colored Petri Nets (TCPN)
|
5
|
+
described using convenient DSL and imperative programming
|
6
|
+
language (Ruby).
|
7
|
+
|
8
|
+
This tool provides library to represent TCPN model and exposing
|
9
|
+
require API. It also provides DSL that facilitates creating the
|
10
|
+
TCPN model.
|
11
|
+
|
12
|
+
TCPN DSL
|
13
|
+
--------
|
14
|
+
|
15
|
+
DSL is meant to enable convenient creation of the TCPN model. It
|
16
|
+
can be interpreted from a file or directly from a block in Ruby
|
17
|
+
program.
|
18
|
+
|
19
|
+
TCPN described using the DSL consists of at least one page, thus
|
20
|
+
the description must start with `page` statement. Inside page,
|
21
|
+
timed and not timed places can be defined using `timed_place` and
|
22
|
+
`place` statements. Objects returned by these statements
|
23
|
+
represent places. Transitions are created using `transition`
|
24
|
+
statement. For each transition one defines its input and output
|
25
|
+
places.
|
26
|
+
|
27
|
+
### Pages
|
28
|
+
|
29
|
+
Pages of TCPN have a name and their content is defined by block
|
30
|
+
of code:
|
31
|
+
|
32
|
+
page "My first model" do
|
33
|
+
# put your model here
|
34
|
+
end
|
35
|
+
|
36
|
+
Page names are used in error messages to facilitate location of
|
37
|
+
problems.
|
38
|
+
|
39
|
+
### Places
|
40
|
+
|
41
|
+
Places of TCPN are identified by their names. Each place can
|
42
|
+
appear on numerous pages. For the sake of efficiency, the tool
|
43
|
+
provides two statements defining places. If tokens in a place
|
44
|
+
should not consider time, one should use normal `place`
|
45
|
+
statement. If time is important and tokens should be delayed in a
|
46
|
+
place if their timestamp requires, one should create such place
|
47
|
+
using `timed_place` statement. Simulation with not timed places
|
48
|
+
is faster, especially for significant number of tokens, thus one
|
49
|
+
should use not timed places wherever it is possible.
|
50
|
+
|
51
|
+
Marking implemented in this tool use hashes to efficiently store,
|
52
|
+
match and remove tokens from places. However, defining proper
|
53
|
+
criterions to index tokens in places is responsibility of the
|
54
|
+
person creating model. Thus defining places, one can pass
|
55
|
+
additional parameter defining keys that will be used to quickly
|
56
|
+
locate tokens in this particular place. For instance if one knows
|
57
|
+
that tokens in place called `:process` will be used in two
|
58
|
+
transitions, first of the need to match process tokens using
|
59
|
+
their `name` and second using value returned by `valid?` method
|
60
|
+
of token value, these two values can be used to index tokens in
|
61
|
+
this place. This place would be created using e.g.
|
62
|
+
|
63
|
+
place :process, name: :name, valid: :valid?
|
64
|
+
|
65
|
+
The first value of pair is the name of the criterion (key),
|
66
|
+
second is the method that will be called on token's value, to
|
67
|
+
obtain actual value used while indexing. Tokens from the above
|
68
|
+
defined place will be be easily accessible using key `:name` and
|
69
|
+
values returned by their method `name` and using key `:valid` and
|
70
|
+
values returned by their method `:valid?`.
|
71
|
+
|
72
|
+
If the method called on token value needs parameter, then the value of
|
73
|
+
in the hash may be an array: [ :method_name, method, parameters ], e.g.:
|
74
|
+
|
75
|
+
place :process, name: :name, valid: :valid?, owner: [ :get_owner, "Jack" ]
|
76
|
+
|
77
|
+
If a place exists on different pages with different keys, the
|
78
|
+
keys from different place definitions will be merged.
|
79
|
+
This method of token indexing is used in both: timed and not
|
80
|
+
timed places.
|
81
|
+
|
82
|
+
|
83
|
+
### Transitions
|
84
|
+
|
85
|
+
Transitions are defined using `transition` statement and
|
86
|
+
identified by name. They must exist on at most one page of the
|
87
|
+
model. Block passed to transition defines its input and output
|
88
|
+
places.
|
89
|
+
|
90
|
+
#### Input places
|
91
|
+
|
92
|
+
This example defines transition with one input place and no
|
93
|
+
output places. Variable `process` passed to the `input` statement
|
94
|
+
must be a `place` object returned by `place` or `timed_place`
|
95
|
+
statement. There are no input arc inscriptions. Read description
|
96
|
+
of `sentry` to see how tokens from input places are used.
|
97
|
+
|
98
|
+
transition "work" do
|
99
|
+
input process
|
100
|
+
end
|
101
|
+
|
102
|
+
#### Output places
|
103
|
+
|
104
|
+
Output places of transition are defined using its `output`
|
105
|
+
statement. This statement gets `place` object (returned by `place`
|
106
|
+
or `timed_place` statement) and a block of code. The block states
|
107
|
+
for output arc expression and is used to decide what token should
|
108
|
+
be put in the output place, for specified binding and simulation clock.
|
109
|
+
|
110
|
+
The block passed to `output` statement gets two parameters:
|
111
|
+
+binding+ and +clock+. The +binding+ is a hash with names of
|
112
|
+
input places as keys and tokens that will be removed from these
|
113
|
+
places as values. Currently only single tokens are removed. Value
|
114
|
+
returned by the block will be treated as value of token that
|
115
|
+
should be put in output place. Or if correct token object is
|
116
|
+
returned it will be put it the place.
|
117
|
+
|
118
|
+
If output place is timed, it is possible to set both: value nad
|
119
|
+
timestamp of the output token. It can be done by returning hash:
|
120
|
+
|
121
|
+
{ val: token_value, ts: token_timestamp }
|
122
|
+
|
123
|
+
If input place is also timed and only timestamp of token should
|
124
|
+
be changed when putting it in output place, this can be done
|
125
|
+
using `with_timestamp` method:
|
126
|
+
|
127
|
+
binding[:cpu].with_timestamp + 100
|
128
|
+
|
129
|
+
To return more then one token to a place just return an Array --
|
130
|
+
all definitions or tokens from the Array will be put into the
|
131
|
+
place's marking.
|
132
|
+
|
133
|
+
Complete definition of two places and transition with one input
|
134
|
+
and one output place can be defined as follows:
|
135
|
+
|
136
|
+
process = place :process, { name: :name, valid: :valid? }
|
137
|
+
done = place :done
|
138
|
+
|
139
|
+
transition "work" do
|
140
|
+
input process
|
141
|
+
output done do
|
142
|
+
binding[:process].name + "_done"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
When fired it will get a process token from the place called
|
147
|
+
`:process` and put in place called `:done` a token with value
|
148
|
+
being a string being the process name with string `"_done"`
|
149
|
+
appended.
|
150
|
+
|
151
|
+
|
152
|
+
#### Sentries (variation of guards)
|
153
|
+
|
154
|
+
For the sake of efficiency, this tool does not implement
|
155
|
+
traditional guard, understood as a method that gets a binding and
|
156
|
+
returns true or false. For TCPNs described using imperative
|
157
|
+
language, this approach requires analysis of whole Cartesian
|
158
|
+
product of input markings.
|
159
|
+
|
160
|
+
Instead a `sentry` can be defined for a transition, that lets the
|
161
|
+
person that create TCPN model implement significantly faster
|
162
|
+
solution for the same problem. The sentry is a block of code that
|
163
|
+
receives markings of subsequent places and is supposed to
|
164
|
+
generate a list of valid bindings. It should do it in the most
|
165
|
+
efficient way, that is best known to the model creator, as he is
|
166
|
+
the person that knows all specifics of modeled reality and token
|
167
|
+
values used. The tool for each place provides access to its
|
168
|
+
marking in random order, to ensure fairness, consequently the
|
169
|
+
only concern of the person implementing sentry is to correctly
|
170
|
+
match tokens. If keys used to index tokens in input places are
|
171
|
+
defined correctly, one can immediately access list of tokens
|
172
|
+
matching selected criterion and iterate.
|
173
|
+
|
174
|
+
The block defining `sentry` takes three arguments:
|
175
|
+
* `marking_for` -- a hash with place names as keys and place
|
176
|
+
markings as values
|
177
|
+
* `clock` -- current value of simulation clock
|
178
|
+
* `result` -- variable used to collect valid bindings.
|
179
|
+
Using information from `marking_for` and `clock` parameters, the
|
180
|
+
block is supposed to push subsequent valid bindings to the
|
181
|
+
`result` variable using `<<` operator. Each valid binding must be
|
182
|
+
of the form of Hash with input place names as keys and token
|
183
|
+
as values:
|
184
|
+
|
185
|
+
result << { process: token1, cpu: cpu5 }
|
186
|
+
|
187
|
+
To get more then one token to fire a transition, you can put
|
188
|
+
array of tokens as value in the mapping passed to the `result`
|
189
|
+
like this:
|
190
|
+
|
191
|
+
result << { process: [ token1, token2 ], cpu: cpu5 }
|
192
|
+
|
193
|
+
In current implementation of the tool, only the first mapping
|
194
|
+
passed to the `result` will be used and the sentry block will not
|
195
|
+
be subsequently evaluated. It is however better to implement it
|
196
|
+
as iteration over all possible valid bindings for possible future
|
197
|
+
uses.
|
198
|
+
|
199
|
+
Example model with one transition that is supposed to match
|
200
|
+
process tokens with correct CPUs, pass process names to `done`
|
201
|
+
place and return delayed CPUs to their place will look as
|
202
|
+
follows.
|
203
|
+
|
204
|
+
|
205
|
+
page "Example model" do
|
206
|
+
p1 = place :process, { name: :name }
|
207
|
+
cpu = timed_place :cpu, { process: :process }
|
208
|
+
p2 = place :done
|
209
|
+
|
210
|
+
transition 'run' do
|
211
|
+
input p1
|
212
|
+
input cpu
|
213
|
+
output p2 do |binding|
|
214
|
+
binding[:process].value.name.to_s + "_done"
|
215
|
+
end
|
216
|
+
output cpu do |binding, clock|
|
217
|
+
binding[:cpu].with_timestamp clock + 100
|
218
|
+
end
|
219
|
+
|
220
|
+
sentry do |marking_for, clock, result|
|
221
|
+
marking_for[:process].each do |p|
|
222
|
+
marking_for[:cpu].each(:process, p.value.name) do |c|
|
223
|
+
result << { process: p, cpu: c }
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
Place `:process` uses key `:name` as an index, it is not however
|
231
|
+
used in this model. As cpus are matched to processes using value
|
232
|
+
returned by `cpu.process` method, the CPUs are indexed by this
|
233
|
+
value. This allows in `sentry` iteration over cpu tokens that
|
234
|
+
match process name selected before:
|
235
|
+
|
236
|
+
marking_for[:cpu].each(:process, p.value.name) do |c|
|
237
|
+
|
238
|
+
In this case the first matching pair is returned as no additional
|
239
|
+
conditions should be met.
|
240
|
+
|
241
|
+
If we know that every process from `:process` place can be
|
242
|
+
used to fire transition and only correct CPU must be matched, we
|
243
|
+
start iteration from processes. If we started from CPUs, we could
|
244
|
+
find some CPUs without matching processes and the sentry would be
|
245
|
+
slower. This reasoning is however responsibility of model
|
246
|
+
creator, as it strongly depends on specifics of the model (one
|
247
|
+
should know that every process has at least one CPU, but some
|
248
|
+
CPUs may match processes that were already served).
|
249
|
+
|
250
|
+
### Pages in pages in pages ...
|
251
|
+
|
252
|
+
One page can contain not only places and transitions, but also
|
253
|
+
definitions of subsequent pages.
|
254
|
+
|
255
|
+
### Subpages from files
|
256
|
+
|
257
|
+
It is possible to load a subpages from separate files using
|
258
|
+
`sub_page` statement:
|
259
|
+
|
260
|
+
page "Test model" do
|
261
|
+
sub_page 'network.rb'
|
262
|
+
sub_page 'cpus.rb'
|
263
|
+
end
|
264
|
+
|
265
|
+
A subpage can also contain subpages loaded from another files.
|
266
|
+
Paths used to locate files mentioned in subpages are interpreted
|
267
|
+
as relative to location of currently interpreted file.
|
268
|
+
|
269
|
+
Using TCPN model
|
270
|
+
----------------
|
271
|
+
|
272
|
+
### Loading model
|
273
|
+
|
274
|
+
The model described by the DSL can be saved in separate file and
|
275
|
+
loaded using `TCPN.read` method:
|
276
|
+
|
277
|
+
tcpn = TCPN.read 'model/example.rb'
|
278
|
+
|
279
|
+
If the model contains subpages in separate files, they will be
|
280
|
+
loaded from paths relative to the location of the `example.rb`
|
281
|
+
file.
|
282
|
+
|
283
|
+
One can also embed the model directly in Ruby program and
|
284
|
+
interpret using `TCPN.model method`
|
285
|
+
|
286
|
+
tcpn = TCPN.model do
|
287
|
+
page "Example model" do
|
288
|
+
# places and transitions here...
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
Model objects returned by these methods can be used to start
|
293
|
+
simulation and define callbacks.
|
294
|
+
|
295
|
+
|
296
|
+
### Place Marking
|
297
|
+
|
298
|
+
After the model is created, place marking can be set. It can be
|
299
|
+
done using one of the following methods.
|
300
|
+
|
301
|
+
* Find place by name and add tokens to it
|
302
|
+
|
303
|
+
tcpn.find_place(:process).add a_process_object
|
304
|
+
tcpn.find_place(:process).add array_of_process_objects
|
305
|
+
|
306
|
+
* Add marking for specified place of the TCPN (old API, derived
|
307
|
+
from tcpn gem)
|
308
|
+
|
309
|
+
tcpn.add_marking_for(:process, a_process_object)
|
310
|
+
|
311
|
+
To check marking, use one of the following methods:
|
312
|
+
|
313
|
+
* Find place by name and iterate over its marking:
|
314
|
+
|
315
|
+
tcpn.find_place(:process).marking.each
|
316
|
+
|
317
|
+
* Get marking of specified place of TCPN (old API, derived
|
318
|
+
from tcpn gem)
|
319
|
+
|
320
|
+
tcpn.marking_for(:process).each
|
321
|
+
|
322
|
+
More details on methods of the model can be found in [API doc for
|
323
|
+
TCPN class](link:FastTCPN/TCPN.html). More details on methods of
|
324
|
+
the place objects can be found in [API doc for Place
|
325
|
+
class](link:FastTCPN/Place.html).
|
326
|
+
|
327
|
+
### Simulation
|
328
|
+
|
329
|
+
Simulation of the model can be started using `sim` method.
|
330
|
+
|
331
|
+
tcpn.sim
|
332
|
+
|
333
|
+
This method will return when simulation is finished.
|
334
|
+
|
335
|
+
Simulation finishes whe there is no more transitions to fire even after
|
336
|
+
advancing simulation clock. It can also be stopped manually by calling
|
337
|
+
`#stop` method:
|
338
|
+
|
339
|
+
tcpn.stop
|
340
|
+
|
341
|
+
This method can be used e.g. in callbacks (see below).
|
342
|
+
|
343
|
+
After simulation has finished, it is possible to check if it was
|
344
|
+
finished manually using `#stopped?` method.
|
345
|
+
|
346
|
+
if tcpn.stopped?
|
347
|
+
puts "Simulation was stopped manually."
|
348
|
+
else
|
349
|
+
puts "Simulation has finished. No more transitions to fire."
|
350
|
+
end
|
351
|
+
|
352
|
+
### Callbacks
|
353
|
+
|
354
|
+
Convenient way to obtain results of simulation is provided by
|
355
|
+
callbacks. They can be defined
|
356
|
+
|
357
|
+
* for transitions (before and after firing)
|
358
|
+
* for places (when adding and when removing tokens)
|
359
|
+
* for clock (before and after changes)
|
360
|
+
|
361
|
+
Callbacks are defined using `TCPN#cb_for` method. First parameter
|
362
|
+
of the method defines if the callback concerns transitions
|
363
|
+
(`:transition`), places (`:place`) or clock (`:clock`). Second
|
364
|
+
optional parameter defines when callback should be fired:
|
365
|
+
|
366
|
+
* for transition it can be either `:before` or `:after`
|
367
|
+
* for place it can be `:add` or `:remove`
|
368
|
+
* for clock it can be either `:before` or `:after`
|
369
|
+
|
370
|
+
If this parameter is omitted, callback will be fired in both cases.
|
371
|
+
|
372
|
+
The block passed to `#cb_for` gets two parameters: first is value
|
373
|
+
of the tag defining when callback is fired (`:before` or `:after`
|
374
|
+
transition is fired, while `:add`ing or `:remove`ing tokens from
|
375
|
+
places). Second parameter is event object holding details of the
|
376
|
+
event. It is:
|
377
|
+
|
378
|
+
* for transition callback -- Transition::Event object with
|
379
|
+
fields:
|
380
|
+
* `transition` -- name of transition being fired
|
381
|
+
* `binding` -- token binding used to fire (`place => token(s)`
|
382
|
+
Hash)
|
383
|
+
* `clock` -- current clock value
|
384
|
+
* `tcpn` -- TCPN object representing network being run
|
385
|
+
* for place callback -- Place::Event
|
386
|
+
* `place` -- name of place being changed
|
387
|
+
* `tokens` -- list of tokens added ore removed
|
388
|
+
* `clock` -- current simulation clock (only for timed places,
|
389
|
+
for not timed this is `nil`
|
390
|
+
* `tcpn` -- TCPN object representing network being run
|
391
|
+
* for clock callback -- TCPN::ClockEvent object
|
392
|
+
* `clock` -- current value of simulation clock
|
393
|
+
* `previous_clock` -- previous value of simulation clock (for
|
394
|
+
`:before` this one is `nil`)
|
395
|
+
* `tcpn` -- TCPN object representing network being run
|
396
|
+
|
397
|
+
## Known Errors and Pitfalls
|
398
|
+
|
399
|
+
### Arrays and Hashes as token values
|
400
|
+
|
401
|
+
Arrays and Hashes are used to describe tokens (their list, values and
|
402
|
+
timestamps). Therefore passing an Array or a Hash that itself should be
|
403
|
+
a token value will not work. If you need to do it, either encapsulate
|
404
|
+
the Array or the Hash into your own class or just create an empty class
|
405
|
+
that extends Array or Hash -- this should work too.
|
406
|
+
|
407
|
+
### Cloning problems
|
408
|
+
|
409
|
+
Due to nature of TCPN based on functional paradigm TCPN tokens
|
410
|
+
are immutable. To achieve this in Ruby (and other imperative
|
411
|
+
languages cloning token values is crucial for correct behavior of
|
412
|
+
TCPN simulation.
|
413
|
+
|
414
|
+
Unfortunately, some Ruby objects cannot be cloned. One example is
|
415
|
+
Enumerator that was already started (#next was called at least
|
416
|
+
once). If you put such value in a token, simulation will fail, as
|
417
|
+
the token will not be cloned. Use your own implementation of
|
418
|
+
enumeration instead. Please report any other built-in classes
|
419
|
+
that cause simulation problems.
|
420
|
+
|
421
|
+
|
422
|
+
### TODO
|
423
|
+
|
424
|
+
* Possible optimization can be achieved by implementing
|
425
|
+
clone-on-write instead of eager cloning every time when a token
|
426
|
+
is passed to user-provided code. We can use
|
427
|
+
https://github.com/dkubb/ice_nine to deeple freeze token
|
428
|
+
objects, catch exception indicating that a modification was
|
429
|
+
tried, clone object and redo the modification on the clone. In
|
430
|
+
short. It is not easy and there is no guarantee that overhead
|
431
|
+
of the solution won't level gains resulting from lazy cloning.
|
432
|
+
* Add traditional guard to enable quick prototyping. This however
|
433
|
+
requires a king of input arc inscriptions, as currently this is
|
434
|
+
implemented in sentry and will not have its place in the
|
435
|
+
traditional guard. Maybe we could allow traditional guards only
|
436
|
+
for transitions that remove single tokens? They will not be
|
437
|
+
recommended way of implementing this anyway.
|
438
|
+
* Currently, HashMarking uses own implementation of
|
439
|
+
`lazy_shuffle` to provide tokens in random order. This is meant
|
440
|
+
to be possibly fast for small number of tokens yielded. It
|
441
|
+
requires benchmarking for cases when most or all tokens from
|
442
|
+
marking should be yielded and probably should be reworked for
|
443
|
+
these cases. I expect it to be a problem for cases when there
|
444
|
+
are tokens in input places, but they do not meed requirements
|
445
|
+
and transition cannot be fired -- then probably all tokens will
|
446
|
+
be checked by sentry of this transition.
|
447
|
+
|
448
|
+
|
449
|
+
|
450
|
+
|
451
|
+
# Copyright
|
452
|
+
|
453
|
+
Copyright (c) 2014-2018 Wojciech Rząsa. See LICENSE.txt for further details. Contact me if interested in different license conditions.
|