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,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'graphviz'
5
+ require 'matrix'
6
+ require 'bigdecimal/ludcmp'
7
+
8
+ class PetriNet::Net < PetriNet::Base
9
+ include LUSolve
10
+ # Human readable name
11
+ attr_accessor :name
12
+ # Storage filename
13
+ attr_accessor :filename
14
+ # Description
15
+ attr_accessor :description
16
+ # List of places
17
+ attr_reader :places
18
+ # List of arcs
19
+ attr_reader :arcs
20
+ # List of transitions
21
+ attr_reader :transitions
22
+ # List of markings
23
+ # !depricated!
24
+ attr_reader :markings
25
+
26
+ # should not be public available attr_reader :objects # Array of all objects in net
27
+ # attr_reader :up_to_date # is true if, and only if, the cached elements are calculated AND the net hasn't changed
28
+
29
+ # Create new Petri Net definition.
30
+ #
31
+ # options may be
32
+ # * name used as a human usable identifier (defaults to 'petri_net')
33
+ # * filename (defaults to the name)
34
+ # * description (defaults to 'Petri Net')
35
+ #
36
+ # Accepts a block and yields itself
37
+ def initialize(options = {}, &block)
38
+ @name = (options[:name] || 'petri_net')
39
+ @filename = (options[:filename] || @name)
40
+ @description = (options[:description] || 'Petri Net')
41
+ @places = {}
42
+ @arcs = {}
43
+ @transitions = {}
44
+ @markings = {}
45
+ @objects = []
46
+ @up_to_date = false
47
+ @w_up_to_date = false
48
+
49
+ yield self unless block.nil?
50
+ end
51
+
52
+ # Adds an object to the Petri Net.
53
+ # You can add
54
+ # * PetriNet::Place
55
+ # * PetriNet::Arc
56
+ # * PetriNet::Transition
57
+ # * Array of these
58
+ #
59
+ # The Objects are added by PetriNet::Net#add_place, PetriNet::Net#add_arc and PetriNet::Net#add_transition, refer to these to get more information on how they are added
60
+ # raises an RuntimeError if a wring Type is given
61
+ #
62
+ # returns itself
63
+ def <<(object)
64
+ return if object.nil? # TODO: WORKAROUND There should never be a nil here, even while merging.
65
+
66
+ case object.class.to_s
67
+ when 'Array'
68
+ object.each { |o| self << o }
69
+ when 'PetriNet::Place'
70
+ add_place(object)
71
+ when 'PetriNet::Arc'
72
+ add_arc(object)
73
+ when 'PetriNet::Transition'
74
+ add_transition(object)
75
+ else
76
+ raise "(PetriNet) Unknown object #{object.class}."
77
+ end
78
+ self
79
+ end
80
+ alias add_object <<
81
+
82
+ # Adds a place to the list of places.
83
+ # Adds the place only if the place is valid and unique in the objects-list of the net
84
+ #
85
+ # This Method changes the structure of the PetriNet, you will have to recalculate all cached functions
86
+ def add_place(place)
87
+ if place.validate && !@places.include?(place.name)
88
+ @places[place.name] = place.id
89
+ @objects[place.id] = place
90
+ place.net = self
91
+ return place.id
92
+ end
93
+ changed_structure
94
+ false
95
+ end
96
+
97
+ # Add an arc to the list of arcs.
98
+ #
99
+ # see PetriNet::Net#add_place
100
+ def add_arc(arc)
101
+ if (arc.validate self) && !@arcs.include?(arc.name)
102
+ arc.update self if arc.need_update? self
103
+ @arcs[arc.name] = arc.id
104
+ @objects[arc.id] = arc
105
+ arc.net = self
106
+ return arc.id
107
+ end
108
+ changed_structure
109
+ false
110
+ end
111
+
112
+ # Add a transition to the list of transitions.
113
+ #
114
+ # see PetriNet::Net#add_place
115
+ def add_transition(transition)
116
+ if transition.validate && !@transitions.include?(transition.name)
117
+ @transitions[transition.name] = transition.id
118
+ @objects[transition.id] = transition
119
+ transition.net = self
120
+ return transition.id
121
+ end
122
+ changed_structure
123
+ false
124
+ end
125
+
126
+ # Returns the place refered by the given name
127
+ # or false if there is no place with this name
128
+ def get_place(name)
129
+ place = @objects[@places[name]]
130
+ place.nil? ? false : place
131
+ end
132
+
133
+ # Returns the transition refered by the given name
134
+ # or false if there is no transition with this name
135
+ def get_transition(name)
136
+ trans = @objects[@transitions[name]]
137
+ trans.nil? ? false : trans
138
+ end
139
+
140
+ # returns the arc refered by the given name
141
+ # or false if there is no arc with this name
142
+ def get_arc(name)
143
+ arc = @objects[@arcs[name]]
144
+ arc.nil? ? false : arc
145
+ end
146
+
147
+ # Is this Petri Net pure?
148
+ # A Petri Net is said to be pure if it has no self-loops.
149
+ def pure?
150
+ raise 'Not implemented yet'
151
+ end
152
+
153
+ # Is this Petri Net ordinary?
154
+ # A Petri Net is said to be ordinary if all of its arc weights are 1's.
155
+ def ordinary?
156
+ raise 'Not implemented yet'
157
+ end
158
+
159
+ # Stringify this Petri Net.
160
+ def to_s
161
+ str =
162
+ %(Petri Net [#{@name}]
163
+ ----------------------------
164
+ Description: #{@description}
165
+ Filename: #{@filename}
166
+
167
+ Places
168
+ ----------------------------
169
+ #{str = ''; @places.each_value { |p| str += @objects[p].to_s + "\n" }; str}
170
+ Transitions
171
+ ----------------------------
172
+ #{str = ''; @transitions.each_value { |t| str += @objects[t].to_s + "\n" }; str}
173
+ Arcs
174
+ ----------------------------
175
+ #{str = ''; @arcs.each_value { |a| str += @objects[a].to_s + "\n" }; str}
176
+ )
177
+ str
178
+ end
179
+
180
+ def to_gv(output = 'png', filename = '')
181
+ g = generate_gv
182
+ filename = "#{@name}_net.png" if filename.empty?
183
+ g.output(png: filename) if output == 'png'
184
+ g.output
185
+ end
186
+
187
+ def generate_gv
188
+ g = GraphViz.new(:G, type: :digraph)
189
+
190
+ @places.each_value do |place|
191
+ gv_node = g.add_nodes(@objects[place].name)
192
+ end
193
+ @transitions.each_value do |transition|
194
+ gv_node = g.add_nodes(@objects[transition].name)
195
+ gv_node.shape = :box
196
+ gv_node.fillcolor = :grey90
197
+ end
198
+ @arcs.each_value do |arc|
199
+ gv_edge = g.add_edges(@objects[arc].source.name, @objects[arc].destination.name)
200
+ end
201
+ g
202
+ end
203
+
204
+ # Generate GraphViz dot string.
205
+ def to_gv
206
+ # General graph options
207
+ str = "digraph #{@name} {\n"
208
+ str += "\t// General graph options\n"
209
+ str += "\trankdir = LR;\n"
210
+ str += "\tsize = \"10.5,7.5\";\n"
211
+ str += "\tnode [ style = filled, fillcolor = white, fontsize = 8.0 ]\n"
212
+ str += "\tedge [ arrowhead = vee, arrowsize = 0.5, fontsize = 8.0 ]\n"
213
+ str += "\n"
214
+
215
+ str += "\t// Places\n"
216
+ str += "\tnode [ shape = circle ];\n"
217
+ @places.each_value { |id| str += @objects[id].to_gv }
218
+ str += "\n"
219
+
220
+ str += "\t// Transitions\n"
221
+ str += "\tnode [ shape = box, fillcolor = grey90 ];\n"
222
+ @transitions.each_value { |id| str += @objects[id].to_gv }
223
+ str += "\n"
224
+
225
+ str += "\t// Arcs\n"
226
+ @arcs.each_value { |id| str += @objects[id].to_gv }
227
+ str += "}\n" # Graph closure
228
+
229
+ str
230
+ end
231
+
232
+ # Merges two PetriNets
233
+ # Places, transitions and arcs are equal if they have the same name and description, arcs need to have the same source and destination too). With this definition of equality the resultung net will have unique ojects.
234
+ # ATTENTION conflicting capabilities and weights will be lost and the properies of the net you merge to will be used in future
235
+ # #TODO add a parameter to affect this!
236
+ def merge(net)
237
+ return self if equal? net
238
+ return false if net.class.to_s != 'PetriNet::Net'
239
+
240
+ self << net.get_objects
241
+ self
242
+ end
243
+
244
+ def reachability_graph
245
+ update unless @up_to_date
246
+ generate_reachability_graph unless @graph && @up_to_date
247
+ @graph
248
+ end
249
+
250
+ def generate_coverability_graph
251
+ startmarkings = get_markings
252
+ @graph = PetriNet::CoverabilityGraph.new(self)
253
+ @graph.add_node current_node = PetriNet::CoverabilityGraph::Node.new(@graph, markings: get_markings, start: true)
254
+
255
+ coverability_helper startmarkings, current_node
256
+
257
+ set_markings startmarkings
258
+ @graph
259
+ end
260
+
261
+ def generate_reachability_graph
262
+ startmarkings = get_markings
263
+ @graph = PetriNet::ReachabilityGraph.new(self)
264
+ @graph.add_node current_node = PetriNet::ReachabilityGraph::Node.new(@graph, markings: get_markings, start: true)
265
+
266
+ reachability_helper startmarkings, current_node
267
+
268
+ set_markings startmarkings
269
+ @graph
270
+ end
271
+
272
+ def generate_weight_function
273
+ @weight = {}
274
+ @arcs.each_value do |id|
275
+ arc = @objects[id]
276
+ @weight[[arc.source.id, arc.destination.id]] = arc.weight
277
+ end
278
+ @w_up_to_date = true
279
+ @weight
280
+ end
281
+
282
+ def w0(x, y)
283
+ generate_weight_function unless @w_up_to_date
284
+ @weight[[x, y]].nil? ? 0 : @weight[[x, y]]
285
+ end
286
+
287
+ def update
288
+ generate_weight_function
289
+ @up_to_date = true
290
+ end
291
+
292
+ # is true if, and only if, the cached elements are calculated AND the net hasn't changed
293
+ def update?
294
+ if @w_up_to_date && true # all up_to_date-caches!!!
295
+ @up_to_date = true
296
+ return @up_to_date
297
+ end
298
+ false
299
+ end
300
+ alias up_to_date update?
301
+
302
+ def get_markings
303
+ @places.map { |_key, pid| @objects[pid].markings.size }
304
+ end
305
+
306
+ def get_marking(places)
307
+ places = [places] unless places.class.to_s == 'Array'
308
+ places.map! { |p| get_place p } if places.first.class.to_s == 'Fixnum'
309
+ res = []
310
+ get_place_list.map { |place| res << ((places.include? place.name) ? 1 : 0) }
311
+ res
312
+ end
313
+
314
+ def set_markings(markings)
315
+ i = 0
316
+ @places.each_value do |pid|
317
+ @objects[pid].set_marking markings[i]
318
+ i += 1
319
+ end
320
+ changed_state
321
+ end
322
+
323
+ def get_place_list
324
+ @places.map { |_key, pid| @objects[pid] }
325
+ end
326
+
327
+ def get_place_from_marking(marking)
328
+ return marking if marking.count(1) != 1
329
+
330
+ get_place_list[marking.index(1)].name
331
+ end
332
+
333
+ def objects_size
334
+ @objects.count { |o| !o.nil? }
335
+ end
336
+
337
+ def objects_include?(object)
338
+ @objects.include?(object)
339
+ end
340
+
341
+ def get_object(id)
342
+ @objects[id]
343
+ end
344
+
345
+ def get_objects
346
+ @objects.clone
347
+ end
348
+
349
+ def objects_find_index(object)
350
+ @objects.find_index object
351
+ end
352
+
353
+ def save(filename)
354
+ File.open(filename, 'w') { |_f| @net.to_yaml }
355
+ end
356
+
357
+ def load(filename)
358
+ @net = YAML.safe_load(File.read(filename))
359
+ end
360
+
361
+ def fire(transition)
362
+ get_transition(transition).fire
363
+ end
364
+
365
+ def delta
366
+ generate_delta if @delta.nil?
367
+ @delta
368
+ end
369
+
370
+ def t_invariants
371
+ delta = self.delta
372
+ zero_vector = []
373
+ delta.row_count.times { zero_vector << 0 }
374
+ zero = BigDecimal('0.0')
375
+ one = BigDecimal('1.0')
376
+
377
+ ps = ludecomp(delta.t.to_a.flatten.map { |i| BigDecimal(i, 16) }, delta.row_count, zero, one)
378
+ x = lusolve(delta.t.to_a.flatten.map { |i| BigDecimal(i, 16) }, zero_vector.map { |i| BigDecimal(i, 16) }, ps, zero)
379
+
380
+ x
381
+ end
382
+
383
+ def s_invariant
384
+ raise 'Not jet implemented'
385
+ end
386
+
387
+ private
388
+
389
+ def generate_delta
390
+ d = Array.new(@places.size) { Array.new(@transitions.size) }
391
+ i = 0
392
+ @places.each do |_p_key, p_value|
393
+ j = 0
394
+ @transitions.each do |_t_key, t_value|
395
+ d[i][j] = w0(t_value, p_value) - w0(p_value, t_value)
396
+ j += 1
397
+ end
398
+ i += 1
399
+ end
400
+ @delta = Matrix[d]
401
+ end
402
+
403
+ def changed_structure
404
+ @w_up_to_date = false
405
+ @up_to_date = false
406
+ end
407
+
408
+ def changed_state
409
+ @up_to_date = false
410
+ end
411
+
412
+ def reachability_helper(markings, source)
413
+ @transitions.each_value do |tid|
414
+ raise PetriNet::ReachabilityGraph::InfinityGraphError if @objects[tid].inputs.empty? && !@objects[tid].outputs.empty?
415
+ next if @objects[tid].inputs.empty?
416
+
417
+ if @objects[tid].fire
418
+ current_node = PetriNet::ReachabilityGraph::Node.new(@graph, markings: get_markings)
419
+ begin
420
+ node_id = @graph.add_node current_node
421
+ rescue StandardError
422
+ @graph.add_node! current_node
423
+ @graph.add_edge PetriNet::ReachabilityGraph::Edge.new(@graph, source: source, destination: current_node)
424
+ infinity_node = PetriNet::ReachabilityGraph::InfinityNode.new(@graph)
425
+ @graph.add_node infinity_node
426
+ @graph.add_edge PetriNet::ReachabilityGraph::Edge.new(@graph, source: current_node, destination: infinity_node)
427
+ next
428
+ end
429
+ current_node = @graph.get_node node_id.abs if node_id < 0
430
+ @graph.add_edge PetriNet::ReachabilityGraph::Edge.new(@graph, source: source, destination: current_node, probability: @objects[tid].probability) # if node_id
431
+ reachability_helper get_markings, current_node if node_id >= 0
432
+ end
433
+ set_markings markings
434
+ end
435
+ end
436
+
437
+ def coverability_helper(markings, source, added_omega = false)
438
+ @transitions.each_value do |tid|
439
+ if @objects[tid].fire
440
+ current_node = PetriNet::ReachabilityGraph::Node.new(@graph, markings: get_markings)
441
+ current_node_id = @graph.add_node current_node
442
+ @graph.add_edge PetriNet::ReachabilityGraph::Edge.new(@graph, source: source, destination: current_node, probability: @objects[tid].probability, transition: @objects[tid].name) if current_node_id >= 0
443
+ omega = false
444
+ if current_node_id != -Float::INFINITY && current_node_id < 0 && @graph.get_node(current_node_id * -1) != current_node
445
+ omega = true
446
+ added_omega_old = added_omega
447
+ added_omega = @graph.get_node(current_node_id * -1).add_omega current_node
448
+ break if added_omega_old == added_omega
449
+
450
+ @graph.add_edge PetriNet::ReachabilityGraph::Edge.new(@graph, source: source, destination: @graph.get_node(current_node_id * -1), probability: @objects[tid].probability, transition: @objects[tid].name)
451
+ end
452
+ coverability_helper get_markings, @graph.get_node(current_node_id.abs), added_omega if (current_node_id >= 0 || !omega) && current_node_id != -Float::INFINITY
453
+ end
454
+ set_markings markings
455
+ end
456
+ end
457
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PetriNet::Place < PetriNet::Base
4
+ # Unique ID
5
+ attr_reader :id
6
+ # Needed to sanitize a Petrinet after merging
7
+ attr_writer :id
8
+ # Human readable name
9
+ attr_accessor :name
10
+ # description
11
+ attr_accessor :description
12
+ # Token capacity
13
+ attr_accessor :capacity
14
+ # List of input-arcs
15
+ attr_reader :inputs
16
+ # List of output-arcs
17
+ attr_reader :outputs
18
+ # Current token
19
+ attr_reader :markings
20
+ # The net this place belongs to
21
+ attr_writer :net
22
+
23
+ # Initialize a new place. Supports block configuration.
24
+ def initialize(options = {}, &block)
25
+ @id = next_object_id
26
+ @name = (options[:name] || "Place#{@id}")
27
+ @description = (options[:description] || "Place #{@id}")
28
+ @capacity = options[:capacity].nil? ? Float::INFINITY : options[:capacity]
29
+ @inputs = []
30
+ @outputs = []
31
+ @markings = []
32
+
33
+ yield self unless block.nil?
34
+ end
35
+
36
+ # Add an input arc
37
+ def add_input(arc)
38
+ @inputs << arc.id unless arc.nil? || !validate_input(arc)
39
+ end
40
+
41
+ # Add an output arc
42
+ def add_output(arc)
43
+ @outputs << arc.id unless arc.nil? || !validate_input(arc)
44
+ end
45
+
46
+ def add_marking(count = 1)
47
+ if count <= @capacity
48
+ count.times do
49
+ @markings << PetriNet::Marking.new
50
+ end
51
+ true
52
+ else
53
+ raise 'Tried to add more markings than possible'
54
+ end
55
+ end
56
+
57
+ def set_marking(count)
58
+ @markings = []
59
+ add_marking count
60
+ end
61
+
62
+ alias + add_marking
63
+
64
+ def remove_marking(count = 1)
65
+ if @markings.size >= count
66
+ ret = @markings.pop(count)
67
+ return ret unless ret.nil?
68
+ else
69
+ raise 'Tried to remove more markings that possible'
70
+ end
71
+ end
72
+ alias - remove_marking
73
+
74
+ # GraphViz ID
75
+ def gv_id
76
+ "P#{@id}"
77
+ end
78
+
79
+ # Validate the setup of this place.
80
+ def validate
81
+ return false if @id.nil? || (@id < 0)
82
+ return false if @name.nil? || (@name.strip.length <= 0)
83
+ return false if @description.nil? || (@description.strip.length <= 0)
84
+ return false if @capacity.nil? || (@capacity < -1)
85
+
86
+ true
87
+ end
88
+
89
+ def pretransitions
90
+ raise 'Not part of a net' if @net.nil?
91
+
92
+ transitions = []
93
+ places << inputs.map { |i| @net.objects[i].source }
94
+ end
95
+
96
+ def posttransitions
97
+ raise 'Not part of a net' if @net.nil?
98
+
99
+ outputs.map { |o| @net.objects[o].source }
100
+ end
101
+
102
+ # Stringify this place.
103
+ def to_s
104
+ "#{@id}: #{@name} (#{@capacity.nil? ? -1 : 0}) #{'*' * @markings.length}"
105
+ end
106
+
107
+ # GraphViz definition
108
+ def to_gv
109
+ "\t#{gv_id} [ label = \"#{@name} #{@markings.size} \" ];\n"
110
+ end
111
+
112
+ def ==(object)
113
+ return true if name == object.name && description = object.description
114
+ end
115
+
116
+ private
117
+
118
+ def validate_input(arc)
119
+ inputs.each do |a|
120
+ return false if (@net.get_objects[a] <=> arc) == 0
121
+ end
122
+ true
123
+ end
124
+
125
+ def validate_output(arc)
126
+ outputs.each do |a|
127
+ return false if (@net.get_objects[a] <=> arc) == 0
128
+ end
129
+ true
130
+ end
131
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PetriNet::ReachabilityGraph::Edge < PetriNet::Graph::Edge
4
+ # Creates an edge for PetriNet::ReachabilityGraph
5
+ def initialize(graph, options = {}, &block)
6
+ super(graph, options)
7
+ yield self unless block.nil?
8
+ end
9
+
10
+ # Validates the data holded by this edge, this will be used while adding the edge to the graph
11
+ def validate
12
+ super
13
+ end
14
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphviz'
4
+
5
+ class PetriNet::ReachabilityGraph < PetriNet::Graph
6
+ def initialize(net, options = {})
7
+ options[:type] = 'Reachability'
8
+ super(net, options)
9
+ self
10
+ end
11
+
12
+ def add_node(node)
13
+ @nodes.each_value do |n|
14
+ raise PetriNet::InfiniteReachabilityGraphError if @objects[n] < node
15
+ rescue ArgumentError
16
+ # Just an InfiniteNode
17
+ end
18
+ super node
19
+ end
20
+
21
+ def add_node!(node)
22
+ super node
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PetriNet::ReachabilityGraph::Node < PetriNet::Graph::Node
4
+ def initialize(graph, options = {}, &block)
5
+ super(graph, options)
6
+ yield self unless block.nil?
7
+ end
8
+ end
9
+
10
+ class PetriNet::ReachabilityGraph::InfinityNode < PetriNet::ReachabilityGraph::Node
11
+ def initialize(graph)
12
+ super(graph, markings: [Float::INFINITY])
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'graph'
4
+ require_relative 'reachability_graph/graph'
5
+ require_relative 'reachability_graph/node'
6
+ require_relative 'reachability_graph/edge'
7
+ # class PetriNet::ReachabilityGraph < PetriNet::Base
8
+ # end