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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +80 -0
- data/MIT-LICENSE +22 -0
- data/README.md +592 -0
- data/Rakefile +28 -0
- data/lib/petri_flow/colored/arc_expression.rb +163 -0
- data/lib/petri_flow/colored/color.rb +40 -0
- data/lib/petri_flow/colored/colored_net.rb +146 -0
- data/lib/petri_flow/colored/guard.rb +104 -0
- data/lib/petri_flow/core/arc.rb +63 -0
- data/lib/petri_flow/core/marking.rb +64 -0
- data/lib/petri_flow/core/net.rb +121 -0
- data/lib/petri_flow/core/place.rb +54 -0
- data/lib/petri_flow/core/token.rb +55 -0
- data/lib/petri_flow/core/transition.rb +88 -0
- data/lib/petri_flow/export/cpn_tools_exporter.rb +322 -0
- data/lib/petri_flow/export/json_exporter.rb +224 -0
- data/lib/petri_flow/export/pnml_exporter.rb +229 -0
- data/lib/petri_flow/export/yaml_exporter.rb +246 -0
- data/lib/petri_flow/export.rb +193 -0
- data/lib/petri_flow/generators/adapters/aasm_adapter.rb +69 -0
- data/lib/petri_flow/generators/adapters/state_machines_adapter.rb +83 -0
- data/lib/petri_flow/generators/state_machine_adapter.rb +47 -0
- data/lib/petri_flow/generators/workflow_generator.rb +176 -0
- data/lib/petri_flow/matrix/analyzer.rb +151 -0
- data/lib/petri_flow/matrix/causation.rb +126 -0
- data/lib/petri_flow/matrix/correlation.rb +79 -0
- data/lib/petri_flow/matrix/crud_event_mapping.rb +74 -0
- data/lib/petri_flow/matrix/lineage.rb +113 -0
- data/lib/petri_flow/matrix/reachability.rb +128 -0
- data/lib/petri_flow/railtie.rb +41 -0
- data/lib/petri_flow/registry.rb +85 -0
- data/lib/petri_flow/simulation/simulator.rb +188 -0
- data/lib/petri_flow/simulation/trace.rb +119 -0
- data/lib/petri_flow/tasks/petri_flow.rake +229 -0
- data/lib/petri_flow/verification/boundedness_checker.rb +127 -0
- data/lib/petri_flow/verification/invariant_checker.rb +144 -0
- data/lib/petri_flow/verification/liveness_checker.rb +153 -0
- data/lib/petri_flow/verification/reachability_analyzer.rb +152 -0
- data/lib/petri_flow/verification_runner.rb +287 -0
- data/lib/petri_flow/version.rb +5 -0
- data/lib/petri_flow/visualization/graphviz.rb +220 -0
- data/lib/petri_flow/visualization/mermaid.rb +191 -0
- data/lib/petri_flow/workflow.rb +228 -0
- data/lib/petri_flow.rb +164 -0
- 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
|