orfeas_petri_flow 0.6.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 (46) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/MIT-LICENSE +22 -0
  4. data/README.md +592 -0
  5. data/Rakefile +28 -0
  6. data/lib/petri_flow/colored/arc_expression.rb +163 -0
  7. data/lib/petri_flow/colored/color.rb +40 -0
  8. data/lib/petri_flow/colored/colored_net.rb +146 -0
  9. data/lib/petri_flow/colored/guard.rb +104 -0
  10. data/lib/petri_flow/core/arc.rb +63 -0
  11. data/lib/petri_flow/core/marking.rb +64 -0
  12. data/lib/petri_flow/core/net.rb +121 -0
  13. data/lib/petri_flow/core/place.rb +54 -0
  14. data/lib/petri_flow/core/token.rb +55 -0
  15. data/lib/petri_flow/core/transition.rb +88 -0
  16. data/lib/petri_flow/export/cpn_tools_exporter.rb +322 -0
  17. data/lib/petri_flow/export/json_exporter.rb +224 -0
  18. data/lib/petri_flow/export/pnml_exporter.rb +229 -0
  19. data/lib/petri_flow/export/yaml_exporter.rb +246 -0
  20. data/lib/petri_flow/export.rb +193 -0
  21. data/lib/petri_flow/generators/adapters/aasm_adapter.rb +69 -0
  22. data/lib/petri_flow/generators/adapters/state_machines_adapter.rb +83 -0
  23. data/lib/petri_flow/generators/state_machine_adapter.rb +47 -0
  24. data/lib/petri_flow/generators/workflow_generator.rb +176 -0
  25. data/lib/petri_flow/matrix/analyzer.rb +151 -0
  26. data/lib/petri_flow/matrix/causation.rb +126 -0
  27. data/lib/petri_flow/matrix/correlation.rb +79 -0
  28. data/lib/petri_flow/matrix/crud_event_mapping.rb +74 -0
  29. data/lib/petri_flow/matrix/lineage.rb +113 -0
  30. data/lib/petri_flow/matrix/reachability.rb +128 -0
  31. data/lib/petri_flow/railtie.rb +41 -0
  32. data/lib/petri_flow/registry.rb +85 -0
  33. data/lib/petri_flow/simulation/simulator.rb +188 -0
  34. data/lib/petri_flow/simulation/trace.rb +119 -0
  35. data/lib/petri_flow/tasks/petri_flow.rake +229 -0
  36. data/lib/petri_flow/verification/boundedness_checker.rb +127 -0
  37. data/lib/petri_flow/verification/invariant_checker.rb +144 -0
  38. data/lib/petri_flow/verification/liveness_checker.rb +153 -0
  39. data/lib/petri_flow/verification/reachability_analyzer.rb +152 -0
  40. data/lib/petri_flow/verification_runner.rb +287 -0
  41. data/lib/petri_flow/version.rb +5 -0
  42. data/lib/petri_flow/visualization/graphviz.rb +220 -0
  43. data/lib/petri_flow/visualization/mermaid.rb +191 -0
  44. data/lib/petri_flow/workflow.rb +228 -0
  45. data/lib/petri_flow.rb +164 -0
  46. metadata +174 -0
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Core
5
+ # Represents a Petri net
6
+ # A Petri net consists of places, transitions, and arcs connecting them
7
+ class Net
8
+ attr_reader :places, :transitions, :arcs, :name
9
+
10
+ def initialize(name: "PetriNet")
11
+ @name = name
12
+ @places = {}
13
+ @transitions = {}
14
+ @arcs = []
15
+ end
16
+
17
+ # Add a place to the net
18
+ def add_place(id:, name: nil, initial_tokens: 0, capacity: Float::INFINITY)
19
+ place = Place.new(
20
+ id: id,
21
+ name: name,
22
+ initial_tokens: initial_tokens,
23
+ capacity: capacity
24
+ )
25
+ @places[id] = place
26
+ place
27
+ end
28
+
29
+ # Add a transition to the net
30
+ def add_transition(id:, name: nil, guard: nil)
31
+ transition = Transition.new(id: id, name: name, guard: guard)
32
+ @transitions[id] = transition
33
+ transition
34
+ end
35
+
36
+ # Add an arc connecting a place and transition
37
+ def add_arc(source_id:, target_id:, weight: 1, expression: nil)
38
+ source = @places[source_id] || @transitions[source_id]
39
+ target = @transitions[target_id] || @places[target_id]
40
+
41
+ raise "Source #{source_id} not found" unless source
42
+ raise "Target #{target_id} not found" unless target
43
+
44
+ arc = Arc.new(source: source, target: target, weight: weight, expression: expression)
45
+ @arcs << arc
46
+
47
+ # Register arc with transition
48
+ if arc.input_arc?
49
+ target.add_input_arc(arc)
50
+ else
51
+ source.add_output_arc(arc)
52
+ end
53
+
54
+ arc
55
+ end
56
+
57
+ # Get current marking
58
+ def current_marking
59
+ marking = Marking.new
60
+ @places.each do |id, place|
61
+ marking.set_tokens(id, place.tokens)
62
+ end
63
+ marking
64
+ end
65
+
66
+ # Set marking (restore state)
67
+ def set_marking(marking)
68
+ @places.each do |id, place|
69
+ place.instance_variable_set(:@tokens, marking.tokens_at(id))
70
+ end
71
+ end
72
+
73
+ # Get all enabled transitions
74
+ def enabled_transitions(context = {})
75
+ @transitions.values.select { |t| t.enabled?(context) }
76
+ end
77
+
78
+ # Fire a transition by id
79
+ def fire_transition(transition_id, context = {})
80
+ transition = @transitions[transition_id]
81
+ raise "Transition #{transition_id} not found" unless transition
82
+
83
+ transition.fire!(context)
84
+ end
85
+
86
+ # Check if net is in a deadlock state (no transitions enabled)
87
+ def deadlocked?(context = {})
88
+ enabled_transitions(context).empty?
89
+ end
90
+
91
+ # Get place by id
92
+ def place(id)
93
+ @places[id]
94
+ end
95
+
96
+ # Get transition by id
97
+ def transition(id)
98
+ @transitions[id]
99
+ end
100
+
101
+ # Get statistics about the net
102
+ def stats
103
+ {
104
+ places: @places.size,
105
+ transitions: @transitions.size,
106
+ arcs: @arcs.size,
107
+ total_tokens: @places.values.sum(&:tokens),
108
+ enabled_transitions: enabled_transitions.size
109
+ }
110
+ end
111
+
112
+ def to_s
113
+ "Net(#{@name}, P=#{@places.size}, T=#{@transitions.size}, A=#{@arcs.size})"
114
+ end
115
+
116
+ def inspect
117
+ "#<PetriFlow::Core::Net name=#{@name} #{stats}>"
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Core
5
+ # Represents a place in a Petri net
6
+ # Places hold tokens and represent states in the system
7
+ class Place
8
+ attr_reader :id, :name, :tokens
9
+ attr_accessor :capacity
10
+
11
+ def initialize(id:, name: nil, initial_tokens: 0, capacity: Float::INFINITY)
12
+ @id = id
13
+ @name = name || id.to_s
14
+ @tokens = initial_tokens
15
+ @capacity = capacity
16
+ end
17
+
18
+ # Add tokens to this place
19
+ def add_tokens(count = 1)
20
+ raise CapacityError, "Cannot add #{count} tokens: would exceed capacity #{@capacity}" if @tokens + count > @capacity
21
+
22
+ @tokens += count
23
+ end
24
+
25
+ # Remove tokens from this place
26
+ def remove_tokens(count = 1)
27
+ raise InsufficientTokensError, "Cannot remove #{count} tokens: only #{@tokens} available" if @tokens < count
28
+
29
+ @tokens -= count
30
+ end
31
+
32
+ # Check if place has at least n tokens
33
+ def has_tokens?(count = 1)
34
+ @tokens >= count
35
+ end
36
+
37
+ # Check if place can accept n tokens
38
+ def can_accept?(count = 1)
39
+ @tokens + count <= @capacity
40
+ end
41
+
42
+ def to_s
43
+ "Place(#{@name}, tokens: #{@tokens})"
44
+ end
45
+
46
+ def inspect
47
+ "#<PetriFlow::Core::Place id=#{@id} name=#{@name} tokens=#{@tokens} capacity=#{@capacity}>"
48
+ end
49
+ end
50
+
51
+ class CapacityError < StandardError; end
52
+ class InsufficientTokensError < StandardError; end
53
+ end
54
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Core
5
+ # Represents a token in a colored Petri net
6
+ # Basic Petri nets just count tokens, colored nets attach data to tokens
7
+ class Token
8
+ attr_reader :id, :color, :data, :timestamp
9
+
10
+ def initialize(id: SecureRandom.uuid, color: :default, data: {}, timestamp: Time.current)
11
+ @id = id
12
+ @color = color
13
+ @data = data
14
+ @timestamp = timestamp
15
+ end
16
+
17
+ # Create a copy of this token with updated data
18
+ def with_data(new_data)
19
+ self.class.new(
20
+ id: SecureRandom.uuid,
21
+ color: @color,
22
+ data: @data.merge(new_data),
23
+ timestamp: Time.current
24
+ )
25
+ end
26
+
27
+ # Create a copy of this token with a different color
28
+ def with_color(new_color)
29
+ self.class.new(
30
+ id: SecureRandom.uuid,
31
+ color: new_color,
32
+ data: @data,
33
+ timestamp: Time.current
34
+ )
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ id: @id,
40
+ color: @color,
41
+ data: @data,
42
+ timestamp: @timestamp
43
+ }
44
+ end
45
+
46
+ def to_s
47
+ "Token(#{@color}, #{@data})"
48
+ end
49
+
50
+ def inspect
51
+ "#<PetriFlow::Core::Token id=#{@id} color=#{@color} data=#{@data}>"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Core
5
+ # Represents a transition in a Petri net
6
+ # Transitions are the active components that consume and produce tokens
7
+ class Transition
8
+ attr_reader :id, :name, :input_arcs, :output_arcs
9
+ attr_accessor :guard
10
+
11
+ def initialize(id:, name: nil, guard: nil)
12
+ @id = id
13
+ @name = name || id.to_s
14
+ @guard = guard
15
+ @input_arcs = []
16
+ @output_arcs = []
17
+ end
18
+
19
+ # Add an input arc from a place
20
+ def add_input_arc(arc)
21
+ @input_arcs << arc
22
+ end
23
+
24
+ # Add an output arc to a place
25
+ def add_output_arc(arc)
26
+ @output_arcs << arc
27
+ end
28
+
29
+ # Check if transition is enabled (can fire)
30
+ # A transition is enabled if all input places have sufficient tokens
31
+ # and all output places can accept tokens
32
+ #
33
+ # @param context [Hash] Context for guard evaluation
34
+ # @option context [Boolean] :ignore_guards Skip guard evaluation (P/T abstraction)
35
+ def enabled?(context = {})
36
+ ignore_guards = context[:ignore_guards]
37
+ return false unless ignore_guards || guard_satisfied?(context)
38
+
39
+ # Check input places have sufficient tokens
40
+ @input_arcs.all? { |arc| arc.source.has_tokens?(arc.weight) } &&
41
+ # Check output places can accept tokens
42
+ @output_arcs.all? { |arc| arc.target.can_accept?(arc.weight) }
43
+ end
44
+
45
+ # Fire the transition
46
+ # Consumes tokens from input places and produces tokens in output places
47
+ def fire!(context = {})
48
+ raise TransitionNotEnabledError, "Transition #{@name} is not enabled" unless enabled?(context)
49
+
50
+ # Remove tokens from input places
51
+ @input_arcs.each { |arc| arc.source.remove_tokens(arc.weight) }
52
+
53
+ # Add tokens to output places
54
+ @output_arcs.each { |arc| arc.target.add_tokens(arc.weight) }
55
+
56
+ # Return the transition for chaining
57
+ self
58
+ end
59
+
60
+ def to_s
61
+ "Transition(#{@name})"
62
+ end
63
+
64
+ def inspect
65
+ "#<PetriFlow::Core::Transition id=#{@id} name=#{@name} " \
66
+ "inputs=#{@input_arcs.size} outputs=#{@output_arcs.size}>"
67
+ end
68
+
69
+ private
70
+
71
+ def guard_satisfied?(context)
72
+ return true unless @guard
73
+
74
+ if @guard.respond_to?(:satisfied?)
75
+ @guard.satisfied?(context)
76
+ elsif @guard.respond_to?(:call)
77
+ @guard.call(context)
78
+ elsif @guard.is_a?(Symbol)
79
+ context[@guard]
80
+ else
81
+ !!@guard
82
+ end
83
+ end
84
+ end
85
+
86
+ class TransitionNotEnabledError < StandardError; end
87
+ end
88
+ end
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rexml/document'
4
+
5
+ module PetriFlow
6
+ module Export
7
+ # Exports Colored Petri Nets to CPN Tools XML format
8
+ # CPN Tools is a popular tool for editing, simulating and analyzing CPNs
9
+ class CpnToolsExporter
10
+ attr_reader :net
11
+
12
+ def initialize(net)
13
+ @net = net
14
+ @place_counter = 0
15
+ @trans_counter = 0
16
+ @arc_counter = 0
17
+ end
18
+
19
+ # Export to CPN Tools XML string
20
+ def to_cpn
21
+ doc = REXML::Document.new
22
+ doc << REXML::XMLDecl.new('1.0', 'UTF-8')
23
+
24
+ # Root workspaceElements element
25
+ workspace = doc.add_element('workspaceElements')
26
+
27
+ # Add generator info
28
+ generator = workspace.add_element('generator', {
29
+ 'tool' => 'PetriFlow',
30
+ 'version' => '1.0',
31
+ 'format' => 'CPN'
32
+ })
33
+
34
+ # Add cpnet element
35
+ cpnet = workspace.add_element('cpnet')
36
+
37
+ # Add globbox (global declarations)
38
+ add_globbox(cpnet)
39
+
40
+ # Add page
41
+ page = add_page(cpnet)
42
+
43
+ # Add color declarations to page
44
+ add_page_attributes(page)
45
+
46
+ # Add places
47
+ @net.places.each do |place_id, place|
48
+ add_place_node(page, place)
49
+ end
50
+
51
+ # Add transitions
52
+ @net.transitions.each do |trans_id, transition|
53
+ add_transition_node(page, transition)
54
+ end
55
+
56
+ # Add arcs
57
+ @net.arcs.each do |arc|
58
+ add_arc_node(page, arc)
59
+ end
60
+
61
+ # Format XML nicely
62
+ formatter = REXML::Formatters::Pretty.new(2)
63
+ formatter.compact = true
64
+ output = String.new
65
+ formatter.write(doc, output)
66
+ output
67
+ end
68
+
69
+ # Save to file
70
+ def save_cpn(filename)
71
+ File.write(filename, to_cpn)
72
+ end
73
+
74
+ private
75
+
76
+ def colored_net?
77
+ @net.is_a?(PetriFlow::Colored::ColoredNet)
78
+ end
79
+
80
+ def add_globbox(cpnet)
81
+ globbox = cpnet.add_element('globbox')
82
+
83
+ # Add standard declarations
84
+ block = globbox.add_element('block', { 'id' => 'id1' })
85
+
86
+ # Add color declarations
87
+ if colored_net? && @net.colors.any?
88
+ @net.colors.each_with_index do |(color_name, color), index|
89
+ add_color_block(block, color_name, color, index + 2)
90
+ end
91
+ else
92
+ # Add default INT color
93
+ add_default_color(block)
94
+ end
95
+ end
96
+
97
+ def add_color_block(block, color_name, color, id_num)
98
+ color_elem = block.add_element('color', { 'id' => "id#{id_num}" })
99
+
100
+ # Color name
101
+ id_elem = color_elem.add_element('id')
102
+ id_elem.text = color_name.to_s.upcase
103
+
104
+ # Color type (record/product type)
105
+ if color.attributes.any?
106
+ record_elem = color_elem.add_element('record')
107
+
108
+ color.attributes.each do |attr_name, attr_type|
109
+ record_field = record_elem.add_element('recordfield')
110
+
111
+ field_id = record_field.add_element('id')
112
+ field_id.text = attr_name.to_s
113
+
114
+ field_type = record_field.add_element('id')
115
+ field_type.text = type_to_cpn_type(attr_type)
116
+ end
117
+ else
118
+ # Simple type
119
+ int_elem = color_elem.add_element('int')
120
+ end
121
+ end
122
+
123
+ def add_default_color(block)
124
+ color_elem = block.add_element('color', { 'id' => 'id2' })
125
+ id_elem = color_elem.add_element('id')
126
+ id_elem.text = 'INT'
127
+ int_elem = color_elem.add_element('int')
128
+ end
129
+
130
+ def type_to_cpn_type(type)
131
+ case type.to_s
132
+ when 'integer', 'int'
133
+ 'INT'
134
+ when 'string'
135
+ 'STRING'
136
+ when 'boolean', 'bool'
137
+ 'BOOL'
138
+ when 'float', 'real'
139
+ 'REAL'
140
+ when 'symbol'
141
+ 'STRING'
142
+ when 'hash'
143
+ 'STRING' # Serialize hash as string
144
+ else
145
+ 'STRING' # Default to string
146
+ end
147
+ end
148
+
149
+ def add_page(cpnet)
150
+ page = cpnet.add_element('page', { 'id' => 'page1' })
151
+
152
+ # Page attributes
153
+ pageattr = page.add_element('pageattr', { 'name' => @net.name })
154
+
155
+ page
156
+ end
157
+
158
+ def add_page_attributes(page)
159
+ # Optional: Add color set references at page level
160
+ end
161
+
162
+ def add_place_node(page, place)
163
+ @place_counter += 1
164
+ place_id = "place_#{@place_counter}"
165
+
166
+ place_elem = page.add_element('place', { 'id' => place_id })
167
+
168
+ # Place text (name)
169
+ text_elem = place_elem.add_element('text')
170
+ text_elem.text = place.name
171
+
172
+ # Place type (color set)
173
+ type_elem = place_elem.add_element('type')
174
+ type_text = type_elem.add_element('text')
175
+
176
+ if colored_net? && @net.colored_places[place.id]
177
+ color = @net.colored_places[place.id][:color]
178
+ type_text.text = color ? color.to_s.upcase : 'INT'
179
+ else
180
+ type_text.text = 'INT'
181
+ end
182
+
183
+ # Initial marking
184
+ if place.tokens > 0 || (colored_net? && @net.token_pools[place.id]&.any?)
185
+ initmark_elem = place_elem.add_element('initmark')
186
+ mark_text = initmark_elem.add_element('text')
187
+
188
+ if colored_net? && @net.token_pools[place.id]&.any?
189
+ # Format colored tokens
190
+ tokens = @net.token_pools[place.id].map { |t| format_token(t) }
191
+ mark_text.text = tokens.join('++')
192
+ else
193
+ mark_text.text = place.tokens.to_s
194
+ end
195
+ end
196
+
197
+ # Position (for graphical layout)
198
+ posattr = place_elem.add_element('posattr', {
199
+ 'x' => (100 + @place_counter * 150).to_s,
200
+ 'y' => '100'
201
+ })
202
+
203
+ # Store mapping for arc creation
204
+ @place_mapping ||= {}
205
+ @place_mapping[place.id] = place_id
206
+ end
207
+
208
+ def format_token(token)
209
+ if token.data.any?
210
+ fields = token.data.map { |k, v| "#{k}=#{format_value(v)}" }
211
+ "{#{fields.join(', ')}}"
212
+ else
213
+ "1"
214
+ end
215
+ end
216
+
217
+ def format_value(value)
218
+ case value
219
+ when String
220
+ "\"#{value}\""
221
+ when Symbol
222
+ "\"#{value}\""
223
+ when Hash
224
+ "\"#{value.to_json}\""
225
+ else
226
+ value.to_s
227
+ end
228
+ end
229
+
230
+ def add_transition_node(page, transition)
231
+ @trans_counter += 1
232
+ trans_id = "trans_#{@trans_counter}"
233
+
234
+ trans_elem = page.add_element('trans', { 'id' => trans_id })
235
+
236
+ # Transition text (name)
237
+ text_elem = trans_elem.add_element('text')
238
+ text_elem.text = transition.name
239
+
240
+ # Guard condition
241
+ if transition.guard
242
+ cond_elem = trans_elem.add_element('cond')
243
+ cond_text = cond_elem.add_element('text')
244
+
245
+ guard_text = if transition.guard.respond_to?(:name) && transition.guard.name
246
+ transition.guard.name
247
+ else
248
+ 'true'
249
+ end
250
+ cond_text.text = "[#{guard_text}]"
251
+ end
252
+
253
+ # Position
254
+ posattr = trans_elem.add_element('posattr', {
255
+ 'x' => (100 + @trans_counter * 150).to_s,
256
+ 'y' => '200'
257
+ })
258
+
259
+ # Store mapping for arc creation
260
+ @trans_mapping ||= {}
261
+ @trans_mapping[transition.id] = trans_id
262
+ end
263
+
264
+ def add_arc_node(page, arc)
265
+ @arc_counter += 1
266
+ arc_id = "arc_#{@arc_counter}"
267
+
268
+ # Determine arc orientation and get IDs
269
+ if arc.input_arc?
270
+ # Place -> Transition
271
+ from_place = @place_mapping[arc.source.id]
272
+ to_trans = @trans_mapping[arc.target.id]
273
+
274
+ arc_elem = page.add_element('arc', {
275
+ 'id' => arc_id,
276
+ 'orientation' => 'PtoT',
277
+ 'order' => @arc_counter.to_s
278
+ })
279
+
280
+ arc_elem.add_element('posattr', { 'x' => '0', 'y' => '0' })
281
+ arc_elem.add_element('fillattr', { 'colour' => 'Black' })
282
+ arc_elem.add_element('lineattr', { 'colour' => 'Black' })
283
+ arc_elem.add_element('textattr', { 'colour' => 'Black' })
284
+
285
+ transend = arc_elem.add_element('transend', { 'idref' => to_trans })
286
+ placeend = arc_elem.add_element('placeend', { 'idref' => from_place })
287
+
288
+ elsif arc.output_arc?
289
+ # Transition -> Place
290
+ from_trans = @trans_mapping[arc.source.id]
291
+ to_place = @place_mapping[arc.target.id]
292
+
293
+ arc_elem = page.add_element('arc', {
294
+ 'id' => arc_id,
295
+ 'orientation' => 'TtoP',
296
+ 'order' => @arc_counter.to_s
297
+ })
298
+
299
+ arc_elem.add_element('posattr', { 'x' => '0', 'y' => '0' })
300
+ arc_elem.add_element('fillattr', { 'colour' => 'Black' })
301
+ arc_elem.add_element('lineattr', { 'colour' => 'Black' })
302
+ arc_elem.add_element('textattr', { 'colour' => 'Black' })
303
+
304
+ transend = arc_elem.add_element('transend', { 'idref' => from_trans })
305
+ placeend = arc_elem.add_element('placeend', { 'idref' => to_place })
306
+ end
307
+
308
+ # Arc annotation (expression or weight)
309
+ annot_elem = arc_elem.add_element('annot')
310
+ annot_text = annot_elem.add_element('text')
311
+
312
+ if arc.expression && arc.expression.respond_to?(:name) && arc.expression.name
313
+ annot_text.text = arc.expression.name
314
+ elsif arc.weight > 1
315
+ annot_text.text = "#{arc.weight}*x"
316
+ else
317
+ annot_text.text = 'x'
318
+ end
319
+ end
320
+ end
321
+ end
322
+ end