petrinet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,284 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
2
+ <pnml>
3
+ <net>
4
+ <token id="Default" red="0" green="0" blue="0"/>
5
+ <place id="wait">
6
+ <graphics>
7
+ <position x="161.0" y="248.0"/>
8
+ </graphics>
9
+ <name>
10
+ <value>P0</value>
11
+ <graphics>
12
+ <offset x="-5.0" y="35.0"/>
13
+ </graphics>
14
+ </name>
15
+ <capacity>
16
+ <value>0</value>
17
+ </capacity>
18
+ <initialMarking>
19
+ <graphics>
20
+ <offset x="0.0" y="0.0"/>
21
+ </graphics>
22
+ <value>Default,3</value>
23
+ </initialMarking>
24
+ </place>
25
+ <place id="before">
26
+ <graphics>
27
+ <position x="366.0" y="274.0"/>
28
+ </graphics>
29
+ <name>
30
+ <value>P0</value>
31
+ <graphics>
32
+ <offset x="-5.0" y="35.0"/>
33
+ </graphics>
34
+ </name>
35
+ <capacity>
36
+ <value>0</value>
37
+ </capacity>
38
+ <initialMarking>
39
+ <graphics>
40
+ <offset x="0.0" y="0.0"/>
41
+ </graphics>
42
+ <value></value>
43
+ </initialMarking>
44
+ </place>
45
+ <place id="after">
46
+ <graphics>
47
+ <position x="599.0" y="273.0"/>
48
+ </graphics>
49
+ <name>
50
+ <value>P0</value>
51
+ <graphics>
52
+ <offset x="-5.0" y="35.0"/>
53
+ </graphics>
54
+ </name>
55
+ <capacity>
56
+ <value>0</value>
57
+ </capacity>
58
+ <initialMarking>
59
+ <graphics>
60
+ <offset x="0.0" y="0.0"/>
61
+ </graphics>
62
+ <value></value>
63
+ </initialMarking>
64
+ </place>
65
+ <place id="free">
66
+ <graphics>
67
+ <position x="481.0" y="145.0"/>
68
+ </graphics>
69
+ <name>
70
+ <value>P0</value>
71
+ <graphics>
72
+ <offset x="-5.0" y="35.0"/>
73
+ </graphics>
74
+ </name>
75
+ <capacity>
76
+ <value>0</value>
77
+ </capacity>
78
+ <initialMarking>
79
+ <graphics>
80
+ <offset x="0.0" y="0.0"/>
81
+ </graphics>
82
+ <value>Default,1</value>
83
+ </initialMarking>
84
+ </place>
85
+ <place id="occupied">
86
+ <graphics>
87
+ <position x="487.0" y="403.0"/>
88
+ </graphics>
89
+ <name>
90
+ <value>P0</value>
91
+ <graphics>
92
+ <offset x="-5.0" y="35.0"/>
93
+ </graphics>
94
+ </name>
95
+ <capacity>
96
+ <value>0</value>
97
+ </capacity>
98
+ <initialMarking>
99
+ <graphics>
100
+ <offset x="0.0" y="0.0"/>
101
+ </graphics>
102
+ <value></value>
103
+ </initialMarking>
104
+ </place>
105
+ <place id="gone">
106
+ <graphics>
107
+ <position x="794.0" y="279.0"/>
108
+ </graphics>
109
+ <name>
110
+ <value>P0</value>
111
+ <graphics>
112
+ <offset x="-5.0" y="35.0"/>
113
+ </graphics>
114
+ </name>
115
+ <capacity>
116
+ <value>0</value>
117
+ </capacity>
118
+ <initialMarking>
119
+ <graphics>
120
+ <offset x="0.0" y="0.0"/>
121
+ </graphics>
122
+ <value></value>
123
+ </initialMarking>
124
+ </place>
125
+ <transition id="leave">
126
+ <graphics>
127
+ <position x="701.0" y="264.0"/>
128
+ </graphics>
129
+ <name>
130
+ <value>T0</value>
131
+ <graphics>
132
+ <offset x="-5.0" y="35.0"/>
133
+ </graphics>
134
+ </name>
135
+ <infiniteServer>
136
+ <value>false</value>
137
+ </infiniteServer>
138
+ <timed>
139
+ <value>false</value>
140
+ </timed>
141
+ <priority>
142
+ <value>1</value>
143
+ </priority>
144
+ <orientation>
145
+ <value>0</value>
146
+ </orientation>
147
+ <rate>
148
+ <value>1</value>
149
+ </rate>
150
+ </transition>
151
+ <transition id="enter">
152
+ <graphics>
153
+ <position x="264.0" y="262.0"/>
154
+ </graphics>
155
+ <name>
156
+ <value>T0</value>
157
+ <graphics>
158
+ <offset x="-5.0" y="35.0"/>
159
+ </graphics>
160
+ </name>
161
+ <infiniteServer>
162
+ <value>false</value>
163
+ </infiniteServer>
164
+ <timed>
165
+ <value>false</value>
166
+ </timed>
167
+ <priority>
168
+ <value>1</value>
169
+ </priority>
170
+ <orientation>
171
+ <value>0</value>
172
+ </orientation>
173
+ <rate>
174
+ <value>1</value>
175
+ </rate>
176
+ </transition>
177
+ <transition id="make_photo">
178
+ <graphics>
179
+ <position x="491.0" y="268.0"/>
180
+ </graphics>
181
+ <name>
182
+ <value>T0</value>
183
+ <graphics>
184
+ <offset x="-5.0" y="35.0"/>
185
+ </graphics>
186
+ </name>
187
+ <infiniteServer>
188
+ <value>false</value>
189
+ </infiniteServer>
190
+ <timed>
191
+ <value>false</value>
192
+ </timed>
193
+ <priority>
194
+ <value>1</value>
195
+ </priority>
196
+ <orientation>
197
+ <value>0</value>
198
+ </orientation>
199
+ <rate>
200
+ <value>1</value>
201
+ </rate>
202
+ </transition>
203
+ <arc id="enter TO occupied" source="enter" target="occupied">
204
+ <arcpath id="" x="274.0" y="277.0" curvePoint="false"/>
205
+ <arcpath id="" x="489.0" y="410.0" curvePoint="false"/>
206
+ <type value="normal"/>
207
+ <inscription>
208
+ <value>Default,1</value>
209
+ </inscription>
210
+ </arc>
211
+ <arc id="enter TO before" source="enter" target="before">
212
+ <arcpath id="" x="274.0" y="277.0" curvePoint="false"/>
213
+ <arcpath id="" x="366.0" y="287.0" curvePoint="false"/>
214
+ <type value="normal"/>
215
+ <inscription>
216
+ <value>Default,1</value>
217
+ </inscription>
218
+ </arc>
219
+ <arc id="leave TO free" source="leave" target="free">
220
+ <arcpath id="" x="701.0" y="279.0" curvePoint="false"/>
221
+ <arcpath id="" x="509.0" y="167.0" curvePoint="false"/>
222
+ <type value="normal"/>
223
+ <inscription>
224
+ <value>Default,1</value>
225
+ </inscription>
226
+ </arc>
227
+ <arc id="make_photo TO after" source="make_photo" target="after">
228
+ <arcpath id="" x="501.0" y="283.0" curvePoint="false"/>
229
+ <arcpath id="" x="599.0" y="287.0" curvePoint="false"/>
230
+ <type value="normal"/>
231
+ <inscription>
232
+ <value>Default,1</value>
233
+ </inscription>
234
+ </arc>
235
+ <arc id="leave TO gone" source="leave" target="gone">
236
+ <arcpath id="" x="711.0" y="279.0" curvePoint="false"/>
237
+ <arcpath id="" x="794.0" y="292.0" curvePoint="false"/>
238
+ <type value="normal"/>
239
+ <inscription>
240
+ <value>Default,1</value>
241
+ </inscription>
242
+ </arc>
243
+ <arc id="before TO make_photo" source="before" target="make_photo">
244
+ <arcpath id="" x="396.0" y="288.0" curvePoint="false"/>
245
+ <arcpath id="" x="491.0" y="283.0" curvePoint="false"/>
246
+ <type value="normal"/>
247
+ <inscription>
248
+ <value>Default,1</value>
249
+ </inscription>
250
+ </arc>
251
+ <arc id="after TO leave" source="after" target="leave">
252
+ <arcpath id="" x="629.0" y="287.0" curvePoint="false"/>
253
+ <arcpath id="" x="701.0" y="279.0" curvePoint="false"/>
254
+ <type value="normal"/>
255
+ <inscription>
256
+ <value>Default,1</value>
257
+ </inscription>
258
+ </arc>
259
+ <arc id="free TO enter" source="free" target="enter">
260
+ <arcpath id="" x="483.0" y="167.0" curvePoint="false"/>
261
+ <arcpath id="" x="274.0" y="277.0" curvePoint="false"/>
262
+ <type value="normal"/>
263
+ <inscription>
264
+ <value>Default,1</value>
265
+ </inscription>
266
+ </arc>
267
+ <arc id="wait TO enter" source="wait" target="enter">
268
+ <arcpath id="" x="191.0" y="265.0" curvePoint="false"/>
269
+ <arcpath id="" x="264.0" y="277.0" curvePoint="false"/>
270
+ <type value="normal"/>
271
+ <inscription>
272
+ <value>Default,1</value>
273
+ </inscription>
274
+ </arc>
275
+ <arc id="occupied TO leave" source="occupied" target="leave">
276
+ <arcpath id="" x="514.0" y="410.0" curvePoint="false"/>
277
+ <arcpath id="" x="701.0" y="279.0" curvePoint="false"/>
278
+ <type value="normal"/>
279
+ <inscription>
280
+ <value>Default,1</value>
281
+ </inscription>
282
+ </arc>
283
+ </net>
284
+ </pnml>
data/exe/petrinet ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require "petrinet"
5
+
6
+ options = { transitions: [] }
7
+ OptionParser.new do |opts|
8
+ opts.banner = "Usage: petrinet [options]"
9
+
10
+ opts.on("-t", "--transition=TRANSITION", "Specify a transition to fire. Can be specified multiple times.") do |t|
11
+ options[:transitions] << t.to_sym
12
+ end
13
+
14
+ opts.on("-o", "--output=PATH", "Where to write the animated gif") do |o|
15
+ options[:output] = o
16
+ end
17
+
18
+ opts.on("-s", "--script=SCRIPT", "Specify a script file") do |o|
19
+ options[:script] = o
20
+ end
21
+ end.parse!
22
+
23
+ pnml = ARGV[0]
24
+ net = Petrinet::Net.from_pnml(IO.read(pnml))
25
+ transitions = options[:transitions]
26
+ output = options[:output]
27
+ if options[:script]
28
+ script = Petrinet::MarkingTransitionScript.new(IO.read(options[:script]))
29
+ net = net.mark(script.marking)
30
+ transitions = script.transitions + transitions
31
+ output ||= options[:script].gsub(/\.txt$/, '.gif')
32
+ end
33
+ net.to_animated_gif(transitions, output)
@@ -0,0 +1,37 @@
1
+ module Petrinet
2
+ class AnimatedGifBuilder
3
+ def initialize(net)
4
+ @net = net
5
+ end
6
+
7
+ def write(transition_names, gif_path)
8
+ @image_number = 0
9
+ Dir.mktmpdir('petrinet-animation') do |tmpdir|
10
+ net = @net
11
+
12
+ write_png(net, tmpdir)
13
+ transition_names.each do |transition_name|
14
+ firing = net.prefire(transition_name)
15
+ write_png(firing, tmpdir)
16
+ net = net.fire(transition_name)
17
+ write_png(net, tmpdir)
18
+ end
19
+
20
+ STDOUT.write "🎬\n"
21
+ `convert -delay 100 -loop 0 #{tmpdir}/*.png #{gif_path}`
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def write_png(net, tmpdir)
28
+ number_string = '%04d' % @image_number
29
+ svg_path = "#{tmpdir}/#{number_string}.svg"
30
+ png_path = "#{tmpdir}/#{number_string}.png"
31
+ File.open(svg_path, 'w:UTF-8') {|io| io.puts(net.to_svg)}
32
+ STDOUT.write "👀️"
33
+ `convert #{svg_path} #{png_path}`
34
+ @image_number += 1
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,178 @@
1
+ require 'tempfile'
2
+ require 'nokogiri'
3
+
4
+ module Petrinet
5
+ class GraphvizBuilder
6
+ def initialize(net, transition_vector_by_transition_name, place_name_by_place_index, state_vector)
7
+ @net = net
8
+ @transition_vectors_by_transition_name = transition_vector_by_transition_name
9
+ @place_name_by_place_index = place_name_by_place_index
10
+ @state_vector = state_vector
11
+ end
12
+
13
+ # Generates an SVG for a net
14
+ def svg
15
+ dot_source = dot
16
+ dotfile = Tempfile.new("petrinet.dot")
17
+ dotfile.write(dot_source)
18
+ dotfile.close
19
+ svgfile = Tempfile.new('petrinet.svg')
20
+ # circo dot fdp neato nop nop1 nop2 osage patchwork sfdp twopi
21
+ `dot -T svg -Kdot #{dotfile.path} -o #{svgfile.path}`
22
+ `cat #{svgfile.path}`
23
+ svg = svgfile.read
24
+ processed_svg(svg)
25
+ end
26
+
27
+ private
28
+
29
+ def processed_svg(svg)
30
+ doc = Nokogiri::XML(svg)
31
+ doc = draw_tokens(doc)
32
+ doc = remove_rectangles(doc)
33
+ doc.to_xml
34
+ end
35
+
36
+ def dot
37
+ transition_vectors_by_transition_name = Hash[@transition_vectors_by_transition_name.sort]
38
+ place_name_by_place_index = Hash[@place_name_by_place_index.sort]
39
+
40
+ dot = <<-EOS
41
+ digraph PetriNet {
42
+ graph [
43
+ bgcolor=white,
44
+ labeljust=l,
45
+ labelloc=t,
46
+ nodesep=0.5,
47
+ penwidth=0,
48
+ ranksep=0.5,
49
+ style=filled
50
+ ];
51
+ node [label="\\N"];
52
+ EOS
53
+
54
+ place_name_by_place_index.each do |place_index, place_name|
55
+ marking = @state_vector[place_index]
56
+ dot += <<-EOS
57
+ subgraph "cluster_place_#{place_name}" {
58
+ graph [
59
+ label="#{place_name}",
60
+ ];
61
+ node [shape=circle];
62
+ "place_#{place_name}" [
63
+ label=#{marking},
64
+ width=0.75
65
+ ];
66
+ }
67
+ EOS
68
+ end
69
+
70
+ transition_vectors_by_transition_name.each do |transition_name, transition_vectors|
71
+ fillcolor = if @net.prefire_transition_name == transition_name
72
+ 'green'
73
+ else
74
+ @net.fireable?(transition_name) ? 'red' : 'black'
75
+ end
76
+
77
+ dot += <<-EOS
78
+ subgraph "cluster_transition_#{transition_name}" {
79
+ graph [
80
+ label="#{transition_name}",
81
+ ];
82
+ node [
83
+ shape=box,
84
+ fillcolor="#{fillcolor}",
85
+ style="solid, filled",
86
+ height=0.1,
87
+ width=0.5
88
+ ];
89
+ "transition_#{transition_name}" [
90
+ label="",
91
+ height=0.1,
92
+ width=0.5
93
+ ];
94
+ }
95
+ EOS
96
+ transition_vectors.each do |transition_vector|
97
+ transition_vector.each_with_index do |direction, place_index|
98
+ place_name = place_name_by_place_index[place_index]
99
+ raise "No place_name for index #{place_index}: #{place_name_by_place_index}" if place_name.nil?
100
+ if direction < 0
101
+ dot += %Q{ "place_#{place_name}" -> "transition_#{transition_name}"\n}
102
+ elsif direction > 0
103
+ dot += %Q{ "transition_#{transition_name}" -> "place_#{place_name}"\n}
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ dot += "}\n"
110
+ dot
111
+ end
112
+
113
+ # Replaces the place labels (which are numbers) with black dots.
114
+ def draw_tokens(doc)
115
+ # place radius (outer)
116
+ pr = 6
117
+ # place radius (inner = with padding)
118
+ pri = pr * 0.8
119
+
120
+ texts = doc.search('text')
121
+ texts.each do |text|
122
+ circle = (text.parent.search('ellipse') || text.parent.search('circle'))[0]
123
+ if circle
124
+ cx = circle[:cx].to_i
125
+ cy = circle[:cy].to_i
126
+ case text.text
127
+ when '0'
128
+ when '1'
129
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx}" cy="#{cy}" r="#{pri}" />}
130
+ when '2'
131
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx - pr}" cy="#{cy}" r="#{pri}" />}
132
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx + pr}" cy="#{cy}" r="#{pri}" />}
133
+ when '3'
134
+ fx_bot = 1
135
+ fy_bot = Math.tan(rad(30))
136
+ fx_top = 0
137
+ fy_top = 1 / Math.cos(rad(30))
138
+
139
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx + fx_top * pr}" cy="#{cy - fy_top * pr}" r="#{pri}" />}
140
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx - fx_bot * pr}" cy="#{cy + fy_bot * pr}" r="#{pri}" />}
141
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx + fx_bot * pr}" cy="#{cy + fy_bot * pr}" r="#{pri}" />}
142
+ when '4'
143
+ fx_bot = fy_bot = fx_top = fy_top = 1
144
+
145
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx - fx_top * pr}" cy="#{cy - fy_top * pr}" r="#{pri}" />}
146
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx + fx_top * pr}" cy="#{cy - fy_top * pr}" r="#{pri}" />}
147
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx - fx_bot * pr}" cy="#{cy + fy_bot * pr}" r="#{pri}" />}
148
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx + fx_bot * pr}" cy="#{cy + fy_bot * pr}" r="#{pri}" />}
149
+ when '5'
150
+ fx_bot = fy_bot = fx_top = fy_top = 2 * Math.sin(rad(45))
151
+
152
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx - fx_top * pr}" cy="#{cy - fy_top * pr}" r="#{pri}" />}
153
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx + fx_top * pr}" cy="#{cy - fy_top * pr}" r="#{pri}" />}
154
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx - fx_bot * pr}" cy="#{cy + fy_bot * pr}" r="#{pri}" />}
155
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx + fx_bot * pr}" cy="#{cy + fy_bot * pr}" r="#{pri}" />}
156
+ text.add_next_sibling %Q{<circle fill="#000000" stroke="none" cx="#{cx}" cy="#{cy}" r="#{pri}" />}
157
+ else
158
+ raise "Cannot draw dots for #{text.text} tokens"
159
+ end
160
+ text.remove
161
+ end
162
+ end
163
+ doc
164
+ end
165
+
166
+ def remove_rectangles(doc)
167
+ polygons = doc.xpath('//svg:polygon[@fill="#ffffff" and @stroke="#000000"]', 'svg' => 'http://www.w3.org/2000/svg')
168
+ polygons.each do |polygon|
169
+ polygon.remove
170
+ end
171
+ doc
172
+ end
173
+
174
+ def rad(y)
175
+ y % 360 * Math::PI / 180
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,30 @@
1
+ module Petrinet
2
+ class MarkingTransitionScript
3
+ def initialize(source)
4
+ @source = source
5
+ end
6
+
7
+ def marking
8
+ pairs = lines.select do |line|
9
+ line =~ /:\d+\s*$/
10
+ end.map do |line|
11
+ parts = line.split(':')
12
+ [parts[0].to_sym, parts[1].to_i]
13
+ end
14
+ Hash[pairs]
15
+ end
16
+
17
+ def transitions
18
+ pairs = lines.reject do |line|
19
+ line =~ /:\d+\s*$/
20
+ end.map(&:to_sym)
21
+ end
22
+
23
+ private
24
+
25
+ def lines
26
+ @source.split(/\n/)
27
+ end
28
+ end
29
+ end
30
+