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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +12 -0
  5. data/CHANGELOG +8 -0
  6. data/Gemfile +9 -0
  7. data/LICENSE +21 -0
  8. data/README.rdoc +97 -0
  9. data/Rakefile +32 -0
  10. data/lib/petri_net/arc.rb +143 -0
  11. data/lib/petri_net/base.rb +30 -0
  12. data/lib/petri_net/coverability_graph/edge.rb +14 -0
  13. data/lib/petri_net/coverability_graph/graph.rb +52 -0
  14. data/lib/petri_net/coverability_graph/node.rb +123 -0
  15. data/lib/petri_net/coverability_graph.rb +8 -0
  16. data/lib/petri_net/graph/edge.rb +64 -0
  17. data/lib/petri_net/graph/graph.rb +324 -0
  18. data/lib/petri_net/graph/node.rb +141 -0
  19. data/lib/petri_net/graph.rb +7 -0
  20. data/lib/petri_net/marking.rb +27 -0
  21. data/lib/petri_net/net.rb +457 -0
  22. data/lib/petri_net/place.rb +131 -0
  23. data/lib/petri_net/reachability_graph/edge.rb +14 -0
  24. data/lib/petri_net/reachability_graph/graph.rb +24 -0
  25. data/lib/petri_net/reachability_graph/node.rb +14 -0
  26. data/lib/petri_net/reachability_graph.rb +8 -0
  27. data/lib/petri_net/transition.rb +135 -0
  28. data/lib/petri_net/version.rb +8 -0
  29. data/lib/petri_net.rb +36 -0
  30. data/petri_net.gemspec +23 -0
  31. data/test/create.rb +64 -0
  32. data/test/reachability_graph/tc_edge.rb +0 -0
  33. data/test/reachability_graph/tc_graph.rb +201 -0
  34. data/test/reachability_graph/tc_node.rb +65 -0
  35. data/test/tc_arc.rb +0 -0
  36. data/test/tc_petri_net.rb +371 -0
  37. data/test/tc_place.rb +0 -0
  38. data/test/tc_transition.rb +7 -0
  39. data/test/ts_all.rb +4 -0
  40. data/test/ts_petri_net.rb +6 -0
  41. data/test/ts_reachability_graph.rb +5 -0
  42. 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'graph/graph'
4
+ require_relative 'graph/node'
5
+ require_relative 'graph/edge'
6
+ class PetriNet::Graph < PetriNet::Base
7
+ 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