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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ # Registry for storing and retrieving workflow classes.
5
+ # Workflows are auto-registered when they inherit from PetriFlow::Workflow.
6
+ #
7
+ # @example Register a workflow manually
8
+ # PetriFlow::Registry.register(RefundRequestWorkflow)
9
+ #
10
+ # @example Get all workflows
11
+ # PetriFlow::Registry.all
12
+ #
13
+ # @example Discover workflows in a directory
14
+ # PetriFlow::Registry.discover_in("app/workflows")
15
+ #
16
+ class Registry
17
+ class << self
18
+ # Register a workflow class
19
+ # @param workflow_class [Class] A class that inherits from PetriFlow::Workflow
20
+ def register(workflow_class)
21
+ workflows[workflow_class.name] = workflow_class
22
+ end
23
+
24
+ # Get a workflow class by name
25
+ # @param name [String, Symbol] The workflow class name
26
+ # @return [Class, nil] The workflow class or nil if not found
27
+ def get(name)
28
+ workflows[name.to_s]
29
+ end
30
+
31
+ # Get all registered workflow classes
32
+ # @return [Array<Class>] All registered workflow classes
33
+ def all
34
+ workflows.values
35
+ end
36
+
37
+ # Get all workflow names
38
+ # @return [Array<String>] All registered workflow class names
39
+ def names
40
+ workflows.keys
41
+ end
42
+
43
+ # Check if a workflow exists
44
+ # @param name [String, Symbol] The workflow class name
45
+ # @return [Boolean] True if workflow is registered
46
+ def exists?(name)
47
+ workflows.key?(name.to_s)
48
+ end
49
+
50
+ # Unregister a workflow
51
+ # @param name [String, Symbol] The workflow class name to remove
52
+ def unregister(name)
53
+ workflows.delete(name.to_s)
54
+ end
55
+
56
+ # Clear all workflows (useful for testing)
57
+ def clear
58
+ @workflows = {}
59
+ end
60
+
61
+ # Count of registered workflows
62
+ # @return [Integer] Number of registered workflows
63
+ def count
64
+ workflows.count
65
+ end
66
+
67
+ # Discover and load workflows from a directory
68
+ # Loads all files matching *_workflow.rb pattern
69
+ # @param directory [String] Path to the workflows directory
70
+ def discover_in(directory)
71
+ return unless directory && Dir.exist?(directory)
72
+
73
+ Dir.glob(File.join(directory, "**", "*_workflow.rb")).sort.each do |file|
74
+ require file
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def workflows
81
+ @workflows ||= {}
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Simulation
5
+ # Simulator for Petri nets
6
+ # Simulates execution and generates traces
7
+ class Simulator
8
+ attr_reader :net, :trace, :current_step
9
+
10
+ def initialize(net)
11
+ @net = net
12
+ @trace = Trace.new
13
+ @current_step = 0
14
+ end
15
+
16
+ # Run simulation for n steps or until no transitions enabled
17
+ def run(steps: 100, strategy: :random, context: {})
18
+ @current_step = 0
19
+ @trace.clear
20
+ @trace.record_initial_marking(@net.current_marking)
21
+
22
+ steps.times do |step|
23
+ @current_step = step
24
+
25
+ enabled = @net.enabled_transitions(context)
26
+ break if enabled.empty?
27
+
28
+ transition = select_transition(enabled, strategy)
29
+ fire_transition(transition, context)
30
+ end
31
+
32
+ @trace
33
+ end
34
+
35
+ # Run until a specific condition is met
36
+ def run_until(strategy: :random, max_steps: 1000, context: {}, &condition)
37
+ @current_step = 0
38
+ @trace.clear
39
+ @trace.record_initial_marking(@net.current_marking)
40
+
41
+ max_steps.times do |step|
42
+ @current_step = step
43
+
44
+ # Check condition
45
+ break if condition.call(@net.current_marking, @net)
46
+
47
+ enabled = @net.enabled_transitions(context)
48
+ break if enabled.empty?
49
+
50
+ transition = select_transition(enabled, strategy)
51
+ fire_transition(transition, context)
52
+ end
53
+
54
+ @trace
55
+ end
56
+
57
+ # Run simulation multiple times (Monte Carlo)
58
+ def run_multiple(runs: 100, steps: 50, strategy: :random, context: {})
59
+ traces = []
60
+ initial_marking = @net.current_marking.dup
61
+
62
+ runs.times do
63
+ # Reset to initial state
64
+ @net.set_marking(initial_marking.dup)
65
+
66
+ # Run simulation
67
+ trace = run(steps: steps, strategy: strategy, context: context)
68
+ traces << trace
69
+
70
+ # Reset for next run
71
+ @net.set_marking(initial_marking.dup)
72
+ end
73
+
74
+ analyze_traces(traces)
75
+ end
76
+
77
+ # Interactive simulation (step by step)
78
+ def step(transition_id = nil, context: {})
79
+ if transition_id
80
+ transition = @net.transition(transition_id)
81
+ raise "Transition #{transition_id} not found" unless transition
82
+ raise "Transition #{transition_id} not enabled" unless transition.enabled?(context)
83
+
84
+ fire_transition(transition, context)
85
+ else
86
+ enabled = @net.enabled_transitions(context)
87
+ return nil if enabled.empty?
88
+
89
+ transition = enabled.first
90
+ fire_transition(transition, context)
91
+ end
92
+
93
+ @current_step += 1
94
+ transition
95
+ end
96
+
97
+ # Reset simulation
98
+ def reset(initial_marking = nil)
99
+ @current_step = 0
100
+ @trace.clear
101
+
102
+ if initial_marking
103
+ @net.set_marking(initial_marking)
104
+ end
105
+
106
+ @trace.record_initial_marking(@net.current_marking)
107
+ end
108
+
109
+ private
110
+
111
+ def select_transition(enabled_transitions, strategy)
112
+ case strategy
113
+ when :random
114
+ enabled_transitions.sample
115
+ when :first
116
+ enabled_transitions.first
117
+ when :priority
118
+ # Transitions with higher priority (more input arcs) first
119
+ enabled_transitions.max_by { |t| t.input_arcs.size }
120
+ when :least_used
121
+ # Select least fired transition
122
+ firing_counts = @trace.transition_firing_counts
123
+ enabled_transitions.min_by { |t| firing_counts[t.id] || 0 }
124
+ else
125
+ enabled_transitions.first
126
+ end
127
+ end
128
+
129
+ def fire_transition(transition, context)
130
+ # Record state before firing
131
+ marking_before = @net.current_marking.dup
132
+
133
+ # Fire transition
134
+ transition.fire!(context)
135
+
136
+ # Record state after firing
137
+ marking_after = @net.current_marking.dup
138
+
139
+ # Add to trace
140
+ @trace.record_transition(
141
+ step: @current_step,
142
+ transition_id: transition.id,
143
+ transition_name: transition.name,
144
+ marking_before: marking_before,
145
+ marking_after: marking_after
146
+ )
147
+ end
148
+
149
+ def analyze_traces(traces)
150
+ {
151
+ total_runs: traces.size,
152
+ average_steps: traces.map(&:steps).sum.to_f / traces.size,
153
+ min_steps: traces.map(&:steps).min,
154
+ max_steps: traces.map(&:steps).max,
155
+ deadlocked_runs: traces.count { |t| t.deadlocked? },
156
+ transition_frequencies: aggregate_transition_frequencies(traces),
157
+ state_coverage: aggregate_state_coverage(traces),
158
+ traces: traces
159
+ }
160
+ end
161
+
162
+ def aggregate_transition_frequencies(traces)
163
+ frequencies = Hash.new(0)
164
+
165
+ traces.each do |trace|
166
+ trace.transition_firing_counts.each do |transition_id, count|
167
+ frequencies[transition_id] += count
168
+ end
169
+ end
170
+
171
+ total_firings = frequencies.values.sum
172
+ frequencies.transform_values { |count| count.to_f / total_firings }
173
+ end
174
+
175
+ def aggregate_state_coverage(traces)
176
+ all_states = Set.new
177
+
178
+ traces.each do |trace|
179
+ trace.visited_markings.each do |marking|
180
+ all_states.add(marking.to_h.sort.to_s)
181
+ end
182
+ end
183
+
184
+ all_states.size
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PetriFlow
4
+ module Simulation
5
+ # Trace of a Petri net execution
6
+ # Records the sequence of transitions fired and markings visited
7
+ class Trace
8
+ attr_reader :transitions, :markings, :initial_marking
9
+
10
+ def initialize
11
+ @transitions = []
12
+ @markings = []
13
+ @initial_marking = nil
14
+ end
15
+
16
+ # Record initial marking
17
+ def record_initial_marking(marking)
18
+ @initial_marking = marking.dup
19
+ @markings << marking.dup
20
+ end
21
+
22
+ # Record a transition firing
23
+ def record_transition(step:, transition_id:, transition_name:, marking_before:, marking_after:)
24
+ @transitions << {
25
+ step: step,
26
+ transition_id: transition_id,
27
+ transition_name: transition_name,
28
+ marking_before: marking_before.dup,
29
+ marking_after: marking_after.dup,
30
+ timestamp: Time.current
31
+ }
32
+
33
+ @markings << marking_after.dup
34
+ end
35
+
36
+ # Get number of steps
37
+ def steps
38
+ @transitions.size
39
+ end
40
+
41
+ # Check if trace ended in deadlock
42
+ def deadlocked?
43
+ # If trace has transitions, check if last marking had no enabled transitions
44
+ # This is a heuristic - in practice we'd need to check the net state
45
+ false # Placeholder - would need net reference
46
+ end
47
+
48
+ # Get transition firing counts
49
+ def transition_firing_counts
50
+ counts = Hash.new(0)
51
+ @transitions.each do |t|
52
+ counts[t[:transition_id]] += 1
53
+ end
54
+ counts
55
+ end
56
+
57
+ # Get visited markings (unique states)
58
+ def visited_markings
59
+ @markings.uniq { |m| m.to_h.sort.to_s }
60
+ end
61
+
62
+ # Get firing sequence (just transition IDs)
63
+ def firing_sequence
64
+ @transitions.map { |t| t[:transition_id] }
65
+ end
66
+
67
+ # Get firing sequence with names
68
+ def firing_sequence_names
69
+ @transitions.map { |t| t[:transition_name] }
70
+ end
71
+
72
+ # Export trace data
73
+ def to_h
74
+ {
75
+ initial_marking: @initial_marking&.to_h,
76
+ steps: steps,
77
+ transitions: @transitions,
78
+ markings: @markings.map(&:to_h),
79
+ firing_sequence: firing_sequence,
80
+ unique_states: visited_markings.size
81
+ }
82
+ end
83
+
84
+ # Pretty print trace
85
+ def to_s
86
+ lines = []
87
+ lines << "Trace (#{steps} steps)"
88
+ lines << "Initial: #{@initial_marking&.to_h}"
89
+ lines << ""
90
+
91
+ @transitions.each_with_index do |transition, idx|
92
+ lines << "Step #{transition[:step]}: #{transition[:transition_name]} (#{transition[:transition_id]})"
93
+ lines << " Before: #{transition[:marking_before].to_h}"
94
+ lines << " After: #{transition[:marking_after].to_h}"
95
+ end
96
+
97
+ lines.join("\n")
98
+ end
99
+
100
+ # Clear trace
101
+ def clear
102
+ @transitions.clear
103
+ @markings.clear
104
+ @initial_marking = nil
105
+ end
106
+
107
+ # Statistics about the trace
108
+ def stats
109
+ {
110
+ steps: steps,
111
+ unique_states: visited_markings.size,
112
+ unique_transitions: transition_firing_counts.size,
113
+ most_fired_transition: transition_firing_counts.max_by { |_, c| c }&.first,
114
+ firing_counts: transition_firing_counts
115
+ }
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ # PetriFlow Verification Tasks
4
+ #
5
+ # These tasks are automatically loaded in Rails apps that include the petri_flow gem.
6
+ # Workflows are discovered in app/workflows/*_workflow.rb
7
+ #
8
+ # Usage:
9
+ # rake petri_flow:verify # Verify all workflows
10
+ # rake petri_flow:verify:list # List registered workflows
11
+ # rake petri_flow:verify:workflow[Name] # Verify specific workflow
12
+ # rake petri_flow:generate:from_state_machine[Model,attr] # Generate from state machine
13
+ # rake petri_flow:generate:scan # Scan for models with state machines
14
+ #
15
+ # rake workflows:verify # Alias for petri_flow:verify
16
+ # rake workflows:list # Alias for petri_flow:verify:list
17
+
18
+ namespace :petri_flow do
19
+ desc "Verify all registered workflows and generate reports"
20
+ task verify: :environment do
21
+ ensure_workflows_loaded
22
+
23
+ if PetriFlow::Registry.count.zero?
24
+ puts "No workflows registered."
25
+ puts "Create workflows in #{workflows_path}/*_workflow.rb"
26
+ exit 1
27
+ end
28
+
29
+ runner = PetriFlow::VerificationRunner.new
30
+ runner.run_all
31
+ end
32
+
33
+ namespace :verify do
34
+ desc "List all registered workflows"
35
+ task list: :environment do
36
+ ensure_workflows_loaded
37
+
38
+ puts "Registered PetriFlow Workflows:"
39
+ puts "-" * 40
40
+
41
+ if PetriFlow::Registry.count.zero?
42
+ puts " No workflows registered."
43
+ puts " Create workflows in #{workflows_path}/*_workflow.rb"
44
+ else
45
+ PetriFlow::Registry.all.each do |workflow_class|
46
+ workflow = workflow_class.new
47
+ places_count = workflow_class.defined_places&.size || 0
48
+ terminal_count = workflow_class.defined_terminal_places&.size || 0
49
+ puts " - #{workflow_class.name}"
50
+ puts " Places: #{places_count}, Terminal: #{terminal_count}"
51
+ end
52
+ end
53
+ end
54
+
55
+ desc "Verify a specific workflow by class name"
56
+ task :workflow, [:name] => :environment do |_t, args|
57
+ ensure_workflows_loaded
58
+
59
+ name = args[:name]
60
+ unless name
61
+ puts "Usage: rake petri_flow:verify:workflow[WorkflowClassName]"
62
+ puts ""
63
+ puts "Available workflows:"
64
+ PetriFlow::Registry.names.each { |n| puts " - #{n}" }
65
+ exit 1
66
+ end
67
+
68
+ unless PetriFlow::Registry.exists?(name)
69
+ puts "Error: Workflow '#{name}' not found."
70
+ puts ""
71
+ puts "Available workflows:"
72
+ PetriFlow::Registry.names.each { |n| puts " - #{n}" }
73
+ exit 1
74
+ end
75
+
76
+ runner = PetriFlow::VerificationRunner.new
77
+ runner.run_by_name(name)
78
+ end
79
+ end
80
+
81
+ desc "Show PetriFlow configuration"
82
+ task config: :environment do
83
+ config = Rails.application.config.petri_flow
84
+
85
+ puts "PetriFlow Configuration:"
86
+ puts "-" * 40
87
+ puts " Workflows path: #{config.workflows_path}"
88
+ puts " Auto-discover: #{config.auto_discover}"
89
+ puts " Full path: #{workflows_path}"
90
+ puts ""
91
+
92
+ ensure_workflows_loaded
93
+ puts "Registered workflows: #{PetriFlow::Registry.count}"
94
+ PetriFlow::Registry.names.each { |n| puts " - #{n}" }
95
+ end
96
+
97
+ namespace :generate do
98
+ desc "Generate a PetriFlow workflow from a model's state machine"
99
+ task :from_state_machine, [:model_class, :state_attr] => :environment do |_t, args|
100
+ model_class_name = args[:model_class]
101
+ state_attr = (args[:state_attr] || :state).to_sym
102
+
103
+ unless model_class_name
104
+ puts "Usage: rake petri_flow:generate:from_state_machine[ModelClass,state_attribute]"
105
+ puts ""
106
+ puts "Examples:"
107
+ puts " rake petri_flow:generate:from_state_machine[Order,state]"
108
+ puts " rake petri_flow:generate:from_state_machine[Spree::Order,state]"
109
+ puts " rake petri_flow:generate:from_state_machine[User,status]"
110
+ puts ""
111
+ puts "Supported state machine libraries:"
112
+ PetriFlow::Generators::WorkflowGenerator.supported_libraries.each do |lib|
113
+ puts " - #{lib}"
114
+ end
115
+ exit 1
116
+ end
117
+
118
+ begin
119
+ model_class = model_class_name.constantize
120
+ rescue NameError
121
+ puts "Error: Model class '#{model_class_name}' not found"
122
+ exit 1
123
+ end
124
+
125
+ unless PetriFlow::Generators::WorkflowGenerator.supports?(model_class)
126
+ puts "Error: #{model_class_name} does not have a supported state machine"
127
+ puts ""
128
+ puts "Supported libraries:"
129
+ PetriFlow::Generators::WorkflowGenerator.supported_libraries.each do |lib|
130
+ puts " - #{lib}"
131
+ end
132
+ exit 1
133
+ end
134
+
135
+ generator = PetriFlow::Generators::WorkflowGenerator.new(model_class, state_attr)
136
+ data = generator.extract
137
+
138
+ puts "=" * 60
139
+ puts "GENERATING WORKFLOW FROM STATE MACHINE"
140
+ puts "=" * 60
141
+ puts ""
142
+ puts "Model: #{model_class_name}"
143
+ puts "State attribute: #{state_attr}"
144
+ puts "Library: #{data[:library]}"
145
+ puts "States: #{data[:states].join(', ')}"
146
+ puts "Initial: #{data[:initial_state]}"
147
+ puts "Terminal: #{data[:terminal_states].join(', ')}"
148
+ puts "Transitions: #{data[:transitions].count}"
149
+ puts ""
150
+
151
+ filepath = generator.generate!
152
+ puts "Generated: #{filepath}"
153
+ puts ""
154
+
155
+ # Verify the generated workflow
156
+ puts "Verifying workflow..."
157
+ results = generator.verify
158
+
159
+ puts " Reachable states: #{results[:reachability][:total_reachable_states]}"
160
+ puts " Is safe (1-bounded): #{results[:boundedness][:is_safe]}"
161
+ puts " Deadlock-free: #{results[:liveness][:deadlock_free]}"
162
+ end
163
+
164
+ desc "List models with supported state machines"
165
+ task scan: :environment do
166
+ puts "Scanning for models with state machines..."
167
+ puts ""
168
+
169
+ found = []
170
+ Rails.application.eager_load! if defined?(Rails)
171
+
172
+ ActiveRecord::Base.descendants.each do |model|
173
+ next if model.abstract_class?
174
+
175
+ library = PetriFlow::Generators::WorkflowGenerator.detect_library(model)
176
+ if library
177
+ found << { model: model.name, library: library }
178
+ end
179
+ end
180
+
181
+ if found.empty?
182
+ puts "No models with supported state machines found."
183
+ puts ""
184
+ puts "Supported libraries:"
185
+ PetriFlow::Generators::WorkflowGenerator.supported_libraries.each do |lib|
186
+ puts " - #{lib}"
187
+ end
188
+ else
189
+ puts "Found #{found.count} model(s) with state machines:"
190
+ puts ""
191
+ puts "| Model | Library |"
192
+ puts "|-------|---------|"
193
+ found.each do |f|
194
+ puts "| #{f[:model]} | #{f[:library]} |"
195
+ end
196
+ puts ""
197
+ puts "Generate with:"
198
+ puts " rake petri_flow:generate:from_state_machine[ModelClass,state]"
199
+ end
200
+ end
201
+ end
202
+
203
+ # Helper methods
204
+
205
+ def ensure_workflows_loaded
206
+ path = workflows_path
207
+ PetriFlow::Registry.discover_in(path) if Dir.exist?(path)
208
+ end
209
+
210
+ def workflows_path
211
+ config = Rails.application.config.petri_flow
212
+ Rails.root.join(config.workflows_path).to_s
213
+ end
214
+ end
215
+
216
+ # Shorthand aliases
217
+ namespace :workflows do
218
+ desc "Verify all workflows (alias for petri_flow:verify)"
219
+ task verify: "petri_flow:verify"
220
+
221
+ desc "List all workflows (alias for petri_flow:verify:list)"
222
+ task list: "petri_flow:verify:list"
223
+
224
+ desc "Generate workflow from state machine (alias)"
225
+ task :from_state_machine, [:model_class, :state_attr] => "petri_flow:generate:from_state_machine"
226
+
227
+ desc "Scan for models with state machines (alias)"
228
+ task scan: "petri_flow:generate:scan"
229
+ end