petri_net_2020 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +12 -0
- data/CHANGELOG +8 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.rdoc +97 -0
- data/Rakefile +32 -0
- data/lib/petri_net/arc.rb +143 -0
- data/lib/petri_net/base.rb +30 -0
- data/lib/petri_net/coverability_graph/edge.rb +14 -0
- data/lib/petri_net/coverability_graph/graph.rb +52 -0
- data/lib/petri_net/coverability_graph/node.rb +123 -0
- data/lib/petri_net/coverability_graph.rb +8 -0
- data/lib/petri_net/graph/edge.rb +64 -0
- data/lib/petri_net/graph/graph.rb +324 -0
- data/lib/petri_net/graph/node.rb +141 -0
- data/lib/petri_net/graph.rb +7 -0
- data/lib/petri_net/marking.rb +27 -0
- data/lib/petri_net/net.rb +457 -0
- data/lib/petri_net/place.rb +131 -0
- data/lib/petri_net/reachability_graph/edge.rb +14 -0
- data/lib/petri_net/reachability_graph/graph.rb +24 -0
- data/lib/petri_net/reachability_graph/node.rb +14 -0
- data/lib/petri_net/reachability_graph.rb +8 -0
- data/lib/petri_net/transition.rb +135 -0
- data/lib/petri_net/version.rb +8 -0
- data/lib/petri_net.rb +36 -0
- data/petri_net.gemspec +23 -0
- data/test/create.rb +64 -0
- data/test/reachability_graph/tc_edge.rb +0 -0
- data/test/reachability_graph/tc_graph.rb +201 -0
- data/test/reachability_graph/tc_node.rb +65 -0
- data/test/tc_arc.rb +0 -0
- data/test/tc_petri_net.rb +371 -0
- data/test/tc_place.rb +0 -0
- data/test/tc_transition.rb +7 -0
- data/test/ts_all.rb +4 -0
- data/test/ts_petri_net.rb +6 -0
- data/test/ts_reachability_graph.rb +5 -0
- metadata +137 -0
@@ -0,0 +1,324 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'graphviz'
|
4
|
+
require 'graphviz/theory'
|
5
|
+
require 'rgl/adjacency'
|
6
|
+
require 'rgl/dijkstra'
|
7
|
+
class PetriNet::InfiniteReachabilityGraphError < RuntimeError
|
8
|
+
end
|
9
|
+
|
10
|
+
class PetriNet::Graph < PetriNet::Base
|
11
|
+
# The PetriNet this graph belongs to
|
12
|
+
attr_reader :net
|
13
|
+
# all nodes from this graph
|
14
|
+
attr_reader :nodes
|
15
|
+
# all edges of this graph
|
16
|
+
attr_reader :edges
|
17
|
+
|
18
|
+
def initialize(net, options = {})
|
19
|
+
@net = net
|
20
|
+
@objects = []
|
21
|
+
@nodes = {}
|
22
|
+
@edges = {}
|
23
|
+
@name = net.name
|
24
|
+
@type = 'Reachability'
|
25
|
+
@unlimited = if options['unlimited'].nil?
|
26
|
+
true
|
27
|
+
else
|
28
|
+
options['unlimited']
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_node(node)
|
33
|
+
if node.validate && (!@objects.include? node)
|
34
|
+
@objects[node.id] = node
|
35
|
+
@nodes[node.name] = node.id
|
36
|
+
node.graph = self
|
37
|
+
return node.id
|
38
|
+
end
|
39
|
+
if @objects.include? node
|
40
|
+
res = (@objects.index node) * -1
|
41
|
+
return res
|
42
|
+
end
|
43
|
+
false
|
44
|
+
end
|
45
|
+
alias add_node! add_node
|
46
|
+
|
47
|
+
def add_edge(edge)
|
48
|
+
if edge.validate && (!@edges.include? edge.name)
|
49
|
+
@objects[edge.id] = edge
|
50
|
+
@edges[edge.name] = edge.id
|
51
|
+
edge.graph = self
|
52
|
+
edge.source.outputs << edge.id
|
53
|
+
edge.destination.inputs << edge.id
|
54
|
+
return edge.id
|
55
|
+
end
|
56
|
+
false
|
57
|
+
end
|
58
|
+
|
59
|
+
def get_edge(source, dest)
|
60
|
+
res = nil
|
61
|
+
@edges.each_value do |edge|
|
62
|
+
if @objects[edge].source == source && @objects[edge].destination == dest
|
63
|
+
res = @objects[edge]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
res
|
67
|
+
end
|
68
|
+
|
69
|
+
# Add an object to the Graph.
|
70
|
+
def <<(object)
|
71
|
+
case object.class.to_s
|
72
|
+
when 'Array'
|
73
|
+
object.each { |o| self << o }
|
74
|
+
when 'PetriNet::ReachabilityGraph::Edge'
|
75
|
+
add_edge(object)
|
76
|
+
when 'PetriNet::ReachabilityGraph::Node'
|
77
|
+
add_node(object)
|
78
|
+
else
|
79
|
+
raise "(PetriNet::ReachabilityGraph) Unknown object #{object.class}."
|
80
|
+
end
|
81
|
+
self
|
82
|
+
end
|
83
|
+
alias add_object <<
|
84
|
+
|
85
|
+
def get_node(node)
|
86
|
+
return @objects[node] if node.class.to_s == 'Fixnum'
|
87
|
+
if node.class.to_s == 'Array'
|
88
|
+
return @objects.select { |o| o.class.to_s == 'PetriNet::ReachabilityGraph::Node' && o.markings == node }.first
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def get_object(id)
|
93
|
+
@objects[id]
|
94
|
+
end
|
95
|
+
|
96
|
+
def get_nodes
|
97
|
+
res = []
|
98
|
+
@nodes.each_value do |n|
|
99
|
+
res << @objects[n]
|
100
|
+
end
|
101
|
+
res
|
102
|
+
end
|
103
|
+
|
104
|
+
def infinite?
|
105
|
+
@nodes.each_value do |node|
|
106
|
+
return true if @objects[node].infinite?
|
107
|
+
end
|
108
|
+
false
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_gv(output = 'png', filename = '')
|
112
|
+
g = generate_gv
|
113
|
+
filename = "#{@name}_graph.png" if filename.empty?
|
114
|
+
g.output(png: filename) if output == 'png'
|
115
|
+
g.output
|
116
|
+
end
|
117
|
+
|
118
|
+
def generate_gv(named = true)
|
119
|
+
g = GraphViz.new(:G, type: :digraph)
|
120
|
+
|
121
|
+
@nodes.each_value do |node|
|
122
|
+
label = if named
|
123
|
+
(@net.get_place_from_marking @objects[node].markings).to_s
|
124
|
+
else
|
125
|
+
@objects[node].markings.to_s
|
126
|
+
end
|
127
|
+
gv_node = g.add_nodes(label)
|
128
|
+
gv_node.set do |n|
|
129
|
+
n.label = '*' + label + '*' if @objects[node].start
|
130
|
+
end
|
131
|
+
end
|
132
|
+
@edges.each_value do |edge|
|
133
|
+
label1 = if named
|
134
|
+
(@net.get_place_from_marking @objects[edge].source.markings).to_s
|
135
|
+
else
|
136
|
+
@objects[edge].source.markings.to_s
|
137
|
+
end
|
138
|
+
|
139
|
+
label2 = if named
|
140
|
+
(@net.get_place_from_marking @objects[edge].destination.markings).to_s
|
141
|
+
else
|
142
|
+
@objects[edge].destination.markings.to_s
|
143
|
+
end
|
144
|
+
gv_edge = g.add_edges(label1, label2)
|
145
|
+
gv_edge.set do |e|
|
146
|
+
e.label = @objects[edge].transition
|
147
|
+
end
|
148
|
+
end
|
149
|
+
@gv = g
|
150
|
+
end
|
151
|
+
|
152
|
+
def generate_rgl
|
153
|
+
g = RGL::DirectedAdjacencyGraph.new
|
154
|
+
@nodes.each_value do |node|
|
155
|
+
g.add_vertex @objects[node].markings.to_s
|
156
|
+
end
|
157
|
+
@edges.each_value do |edge|
|
158
|
+
g.add_edge @objects[edge].source.markings.to_s, @objects[edge].destination.markings.to_s
|
159
|
+
end
|
160
|
+
@rgl = g
|
161
|
+
end
|
162
|
+
|
163
|
+
def to_s
|
164
|
+
str = "#{@type} Graph [#{@name}]\n"
|
165
|
+
str += "----------------------------\n"
|
166
|
+
str += "Description: #{@description}\n"
|
167
|
+
str += "Filename: #{@filename}\n"
|
168
|
+
str += "\n"
|
169
|
+
|
170
|
+
str += "Nodes\n"
|
171
|
+
str += "----------------------------\n"
|
172
|
+
@nodes.each_value { |p| str += @objects[p].to_s + "\n" }
|
173
|
+
str += "\n"
|
174
|
+
|
175
|
+
str += "Edges\n"
|
176
|
+
str += "----------------------------\n"
|
177
|
+
@edges.each_value { |t| str += @objects[t].to_s + "\n" }
|
178
|
+
str += "\n"
|
179
|
+
|
180
|
+
str
|
181
|
+
end
|
182
|
+
|
183
|
+
def get_rgl
|
184
|
+
generate_rgl if @rgl.nil?
|
185
|
+
@rgl
|
186
|
+
end
|
187
|
+
|
188
|
+
def cycles
|
189
|
+
get_rgl.cycles
|
190
|
+
end
|
191
|
+
|
192
|
+
def shortest_path(start, destination)
|
193
|
+
g = get_rgl
|
194
|
+
weights = ->(_edge) { 1 }
|
195
|
+
g.dijkstra_shortest_path(weights, start.to_s, destination.to_s)
|
196
|
+
end
|
197
|
+
|
198
|
+
def path_probability(path)
|
199
|
+
sanitize_probabilities
|
200
|
+
prob = 1
|
201
|
+
counter = 0
|
202
|
+
path.each do |node|
|
203
|
+
edge = get_edge(path[counter + 1], node)
|
204
|
+
prob *= edge.probability unless edge.nil? # last node has no pre-edge
|
205
|
+
counter = counter += 1
|
206
|
+
end
|
207
|
+
prob
|
208
|
+
end
|
209
|
+
|
210
|
+
def node_probability(start, node)
|
211
|
+
paths = get_paths_without_loops(start, node)
|
212
|
+
prob = 0
|
213
|
+
paths.each do |path|
|
214
|
+
prob += (path_probability path)
|
215
|
+
end
|
216
|
+
prob
|
217
|
+
end
|
218
|
+
|
219
|
+
def best_path(start, node)
|
220
|
+
paths = get_paths_without_loops(start, node)
|
221
|
+
prob = 0
|
222
|
+
res_path = nil
|
223
|
+
paths.each do |path|
|
224
|
+
if (path_probability path) >= prob
|
225
|
+
prob = (path_probability path)
|
226
|
+
res_path = path
|
227
|
+
end
|
228
|
+
end
|
229
|
+
[res_path, prob]
|
230
|
+
end
|
231
|
+
|
232
|
+
def worst_path(start, node)
|
233
|
+
paths = get_paths_without_loops(start, node)
|
234
|
+
prob = 1
|
235
|
+
res_path = nil
|
236
|
+
paths.each do |path|
|
237
|
+
if (path_probability path) <= prob
|
238
|
+
prob = (path_probability path)
|
239
|
+
res_path = path
|
240
|
+
end
|
241
|
+
end
|
242
|
+
[res_path, prob]
|
243
|
+
end
|
244
|
+
|
245
|
+
def get_most_relative_influence_on_path(start, node, factor = 0.5)
|
246
|
+
prob_counter = 0
|
247
|
+
prob_diff = []
|
248
|
+
paths = get_paths_without_loops(start, node)
|
249
|
+
paths.each do |path|
|
250
|
+
counter = 0
|
251
|
+
path.each do |curr_node|
|
252
|
+
prob_before = node_probability start, node
|
253
|
+
next if node == path[-1]
|
254
|
+
|
255
|
+
edge = get_edge(path[counter + 1], curr_node)
|
256
|
+
save = edge.probability unless edge.nil?
|
257
|
+
edge.probability += (1 - edge.probability) * factor unless edge.nil?
|
258
|
+
prob_after = node_probability start, node unless edge.nil?
|
259
|
+
edge.probability = save unless edge.nil?
|
260
|
+
prob_diff[prob_counter] = [prob_after - prob_before, [curr_node, path[counter + 1]]] unless edge.nil?
|
261
|
+
prob_counter += 1 unless edge.nil?
|
262
|
+
counter += 1
|
263
|
+
end
|
264
|
+
end
|
265
|
+
prob_diff.max { |x, y| x[0] <=> y[0] }
|
266
|
+
end
|
267
|
+
|
268
|
+
def get_most_absolute_influence_on_path(start, node, sum = 0.01)
|
269
|
+
prob_counter = 0
|
270
|
+
prob_diff = []
|
271
|
+
paths = get_paths_without_loops(start, node)
|
272
|
+
paths.each do |path|
|
273
|
+
counter = 0
|
274
|
+
path.each do |curr_node|
|
275
|
+
prob_before = node_probability start, node
|
276
|
+
next if node == path[-1]
|
277
|
+
|
278
|
+
edge = get_edge(path[counter + 1], curr_node)
|
279
|
+
save = edge.probability unless edge.nil?
|
280
|
+
edge.probability = edge.probability + sum unless edge.nil?
|
281
|
+
edge.probability = 1 if !edge.nil? && edge.probability > 1
|
282
|
+
prob_after = node_probability start, node unless edge.nil?
|
283
|
+
edge.probability = save unless edge.nil?
|
284
|
+
prob_diff[prob_counter] = [prob_after - prob_before, [curr_node, path[counter + 1]]] unless edge.nil?
|
285
|
+
prob_counter += 1 unless edge.nil?
|
286
|
+
counter += 1
|
287
|
+
end
|
288
|
+
end
|
289
|
+
prob_diff.max { |x, y| x[0] <=> y[0] }
|
290
|
+
end
|
291
|
+
|
292
|
+
def get_paths_without_loops(start, goal)
|
293
|
+
get_paths_without_loops_helper(get_node(start), get_node(goal))
|
294
|
+
end
|
295
|
+
|
296
|
+
def sanitize_probabilities
|
297
|
+
@nodes.each_value do |node|
|
298
|
+
prob = 1.0
|
299
|
+
@objects[node].outputs.each do |edge|
|
300
|
+
prob += @objects[edge].probability unless @objects[edge].probability.nil?
|
301
|
+
end
|
302
|
+
@objects[node].outputs.each do |edge|
|
303
|
+
@objects[edge].probability = prob / @objects[node].outputs.size if @objects[edge].probability.nil?
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
private
|
309
|
+
|
310
|
+
def get_paths_without_loops_helper(start, goal, reverse_paths = [], reverse_path = [])
|
311
|
+
if goal == start
|
312
|
+
reverse_path << goal
|
313
|
+
reverse_paths << reverse_path
|
314
|
+
return nil
|
315
|
+
end
|
316
|
+
return nil if reverse_path.include? goal
|
317
|
+
|
318
|
+
path = []
|
319
|
+
goal.inputs.each do |input|
|
320
|
+
get_paths_without_loops_helper(start, @objects[input].source, reverse_paths, reverse_path.clone << goal)
|
321
|
+
end
|
322
|
+
reverse_paths
|
323
|
+
end
|
324
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class PetriNet::Graph::Node < PetriNet::Base
|
4
|
+
include Comparable
|
5
|
+
|
6
|
+
# human readable name
|
7
|
+
attr_reader :name
|
8
|
+
# unique ID
|
9
|
+
attr_reader :id
|
10
|
+
# Makking this node represents
|
11
|
+
attr_reader :markings
|
12
|
+
# The graph this node belongs to
|
13
|
+
attr_accessor :graph
|
14
|
+
# Omega-marked node (unlimited Petrinet -> coverabilitygraph)
|
15
|
+
attr_reader :omega_marked
|
16
|
+
# Incoming edges
|
17
|
+
attr_reader :inputs
|
18
|
+
# Outgoing edges
|
19
|
+
attr_reader :outputs
|
20
|
+
# Label of the node
|
21
|
+
attr_reader :label
|
22
|
+
# True if this is the start-marking
|
23
|
+
attr_reader :start
|
24
|
+
|
25
|
+
def initialize(graph, options = {}, &block)
|
26
|
+
@graph = graph
|
27
|
+
@id = next_object_id
|
28
|
+
@name = (options[:name] || "Node#{@id}")
|
29
|
+
@description = (options[:description] || "Node #{@id}")
|
30
|
+
@inputs = []
|
31
|
+
@outputs = []
|
32
|
+
@label = (options[:label] || @name)
|
33
|
+
@markings = options[:markings]
|
34
|
+
@start = (options[:start] || false)
|
35
|
+
raise ArgumentError, 'Every Node needs markings' if @markings.nil?
|
36
|
+
|
37
|
+
@omega_marked = if @markings.include? Float::INFINITY
|
38
|
+
true
|
39
|
+
else
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
yield self unless block.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
def infinite?
|
47
|
+
@omega_marked
|
48
|
+
end
|
49
|
+
|
50
|
+
# Add an omega-marking to a specified place
|
51
|
+
def add_omega(object)
|
52
|
+
ret = []
|
53
|
+
if object.class.to_s == 'PetriNet::CoverabilityGraph::Node'
|
54
|
+
if self < object
|
55
|
+
counter = 0
|
56
|
+
object.markings.each do |marking|
|
57
|
+
if @markings[counter] < marking
|
58
|
+
@markings[counter] = Float::INFINITY
|
59
|
+
ret << counter
|
60
|
+
end
|
61
|
+
counter += 1
|
62
|
+
end
|
63
|
+
else
|
64
|
+
return false
|
65
|
+
end
|
66
|
+
elsif object.class.to_s == 'Array'
|
67
|
+
object.each do |place|
|
68
|
+
markings[place] = Float::INFINITY
|
69
|
+
ret = object
|
70
|
+
end
|
71
|
+
elsif object.class.to_s == 'Fixnum'
|
72
|
+
markings[object] = Float::INFINITY
|
73
|
+
ret = [object]
|
74
|
+
elsif object.class.to_s == 'PetriNet::ReachabilityGraph::Node'
|
75
|
+
raise PetriNet::Graph::InfinityError('ReachabilityGraphs do not support omega-markings')
|
76
|
+
end
|
77
|
+
@omega_marked = true
|
78
|
+
ret
|
79
|
+
end
|
80
|
+
|
81
|
+
def include_place(place)
|
82
|
+
places = @graph.net.get_place_list
|
83
|
+
included_places = []
|
84
|
+
i = 0
|
85
|
+
@markings.each do |m|
|
86
|
+
included_places << places[i] if m > 0
|
87
|
+
i += 1
|
88
|
+
end
|
89
|
+
included_places.include? place
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate
|
93
|
+
true
|
94
|
+
end
|
95
|
+
|
96
|
+
def gv_id
|
97
|
+
"N#{@id}"
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_gv
|
101
|
+
"\t#{gv_id} [ label = \"#{@markings}\" ];\n"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Compare-operator, other Operators are available through comparable-mixin
|
105
|
+
def <=>(object)
|
106
|
+
return nil unless object.class.to_s == 'PetriNet::ReachabilityGraph::Node'
|
107
|
+
return 0 if @markings == object.markings
|
108
|
+
|
109
|
+
counter = 0
|
110
|
+
less = true
|
111
|
+
markings.each do |marking|
|
112
|
+
if marking <= object.markings[counter] && less
|
113
|
+
less = true
|
114
|
+
else
|
115
|
+
less = false
|
116
|
+
break
|
117
|
+
end
|
118
|
+
counter += 1
|
119
|
+
end
|
120
|
+
return -1 if less
|
121
|
+
|
122
|
+
counter = 0
|
123
|
+
more = true
|
124
|
+
markings.each do |marking|
|
125
|
+
if marking >= object.markings[counter] && more
|
126
|
+
more = true
|
127
|
+
else
|
128
|
+
more = false
|
129
|
+
break
|
130
|
+
end
|
131
|
+
counter += 1
|
132
|
+
end
|
133
|
+
return 1 if more
|
134
|
+
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def to_s
|
139
|
+
"#{@id}: #{@name} (#{@markings})"
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PetriNet
|
4
|
+
# Marking
|
5
|
+
class Marking < PetriNet::Base
|
6
|
+
# depricated
|
7
|
+
attr_accessor :id # Unique ID
|
8
|
+
# depricated
|
9
|
+
attr_accessor :name # Human readable name
|
10
|
+
# depricated
|
11
|
+
attr_accessor :description # Description
|
12
|
+
# depricated
|
13
|
+
attr_accessor :timestep # Marking timestep
|
14
|
+
|
15
|
+
# Create a new marking.
|
16
|
+
# depricated
|
17
|
+
def initialize(_options = {}, &block)
|
18
|
+
yield self unless block.nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
# Validate this marking.
|
22
|
+
def validate; end
|
23
|
+
|
24
|
+
# Stringify this marking.
|
25
|
+
def to_s; end
|
26
|
+
end
|
27
|
+
end
|