petrinet 0.1.0

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