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.
- 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.
|