petrinet 0.1.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.
@@ -0,0 +1,158 @@
1
+ require 'set'
2
+ require_relative 'pnml_builder'
3
+ require_relative 'graphviz_builder'
4
+ require_relative 'animated_gif_builder'
5
+
6
+ module Petrinet
7
+ # Represents a Petri Net in a particular state. Instances of this class are immutable. The following methods return
8
+ # new instances:
9
+ #
10
+ # * mark
11
+ # * fire
12
+ #
13
+ # The internal representation uses a VASS - https://en.wikipedia.org/wiki/Vector_addition_system
14
+ # A good explanation of how this works with Petri Nets is here: https://github.com/bitwrap/bitwrap-io/blob/master/whitepaper.md
15
+ #
16
+ class Net
17
+ def self.from_pnml(xml)
18
+ builder = PnmlBuilder.new(xml)
19
+ builder.net
20
+ end
21
+
22
+ def self.build(&proc)
23
+ builder = Builder.new
24
+ proc.call(builder)
25
+ builder.net
26
+ end
27
+
28
+ attr_reader :prefire_transition_name
29
+
30
+ def initialize(state_vector, place_index_by_place_name, transition_vectors_by_transition_name, prefire_transition_name = nil)
31
+ @state_vector = state_vector
32
+ @place_index_by_place_name = place_index_by_place_name
33
+ @transition_vectors_by_transition_name = transition_vectors_by_transition_name
34
+ @prefire_transition_name = prefire_transition_name
35
+ freeze
36
+ end
37
+
38
+ # Marks the petri net and returns a new instance
39
+ def mark(marking)
40
+ new_state_vector = Array.new(@state_vector.size, 0)
41
+ marking.each do |place_name, token_count|
42
+ index = @place_index_by_place_name[place_name]
43
+ new_state_vector[index] = token_count
44
+ end
45
+ self.class.new(new_state_vector, @place_index_by_place_name, @transition_vectors_by_transition_name)
46
+ end
47
+
48
+ def fire(transition_name)
49
+ new_state_vector = new_state_vector(transition_name)
50
+ self.class.new(new_state_vector, @place_index_by_place_name, @transition_vectors_by_transition_name)
51
+ end
52
+
53
+ def prefire(transition_name)
54
+ self.class.new(@state_vector, @place_index_by_place_name, @transition_vectors_by_transition_name, transition_name)
55
+ end
56
+
57
+ def fireable?(transition_name)
58
+ begin
59
+ new_state_vector(transition_name)
60
+ true
61
+ rescue
62
+ false
63
+ end
64
+ end
65
+
66
+ def fireable
67
+ result = Set.new
68
+ @transition_vectors_by_transition_name.keys.each do |transition_name|
69
+ begin
70
+ fire(transition_name)
71
+ result.add(transition_name)
72
+ rescue => ignore
73
+ # It wasn't fireable - ignore it
74
+ end
75
+ end
76
+ result
77
+ end
78
+
79
+ def to_svg
80
+ builder = GraphvizBuilder.new(self, @transition_vectors_by_transition_name, @place_index_by_place_name.invert, @state_vector)
81
+ builder.svg
82
+ end
83
+
84
+ def to_animated_gif(transition_names, gif_path)
85
+ builder = AnimatedGifBuilder.new(self)
86
+ builder.write(transition_names, gif_path)
87
+ end
88
+
89
+ private
90
+
91
+ def new_state_vector(transition_name)
92
+ transition_vectors = @transition_vectors_by_transition_name[transition_name]
93
+ raise "Unknown transition: #{transition_name}. Known transitions: #{@transition_vectors_by_transition_name.keys}" if transition_vectors.nil?
94
+ transition_vectors.reduce(@state_vector) do |state_vector, transition_vector|
95
+ v = state_vector.zip(transition_vector).map { |s, t| s + t }
96
+ raise "Cannot fire: #{transition_name}" unless valid?(v)
97
+ v
98
+ end
99
+ end
100
+
101
+ def valid?(state_vector)
102
+ !(state_vector.any? { |s| s.negative? })
103
+ end
104
+
105
+ class Builder
106
+ def initialize
107
+ @place_names = Set.new
108
+ @transition_by_name = Hash.new
109
+ @state_vector = []
110
+ end
111
+
112
+ def transition(transition_name, arcs)
113
+ take_place_names = [arcs[:take]].flatten
114
+ give_place_names = [arcs[:give]].flatten
115
+ @place_names.merge(take_place_names)
116
+ @place_names.merge(give_place_names)
117
+ @transition_by_name[transition_name] = Transition.new(take_place_names, give_place_names)
118
+ end
119
+
120
+ def net
121
+ place_index_by_place_name = Hash.new do |h, k|
122
+ index = h.size
123
+ @state_vector[index] = 0
124
+ h[k] = index
125
+ end
126
+
127
+ transition_vectors_by_transition_name_pairs = @transition_by_name.map do |transition_name, transition|
128
+ [transition_name, transition.to_vectors(@place_names.size, place_index_by_place_name)]
129
+ end
130
+ transition_vectors_by_transition_name = Hash[transition_vectors_by_transition_name_pairs]
131
+
132
+ Net.new(@state_vector.freeze, place_index_by_place_name.freeze, transition_vectors_by_transition_name.freeze)
133
+ end
134
+
135
+ class Transition
136
+ def initialize(take_place_names, give_place_names)
137
+ @take_place_names = take_place_names
138
+ @give_place_names = give_place_names
139
+ end
140
+
141
+ def to_vectors(size, place_index_by_place_name)
142
+ take_vector = Array.new(size, 0)
143
+ @take_place_names.each do |take_place_name|
144
+ index = place_index_by_place_name[take_place_name]
145
+ take_vector[index] -= 1
146
+ end
147
+
148
+ give_vector = Array.new(size, 0)
149
+ @give_place_names.each do |give_place_name|
150
+ index = place_index_by_place_name[give_place_name]
151
+ give_vector[index] += 1
152
+ end
153
+ [take_vector, give_vector]
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,71 @@
1
+ require 'nokogiri'
2
+
3
+ module Petrinet
4
+ class PnmlBuilder
5
+ def initialize(xml)
6
+ @xml = xml
7
+ end
8
+
9
+ def net
10
+ doc = Nokogiri::XML(@xml)
11
+
12
+ transition_by_id = {}
13
+ place_by_id = {}
14
+ edges = []
15
+
16
+ transitions = Hash.new {|h, k| h[k] = {take: [], give: []}}
17
+ markings = Hash.new
18
+
19
+ # https://github.com/bitwrap/bitwrap-io/blob/master/bitwrap_io/machine/pnml.py#L216
20
+
21
+ doc.xpath('//place').each do |place_node|
22
+ place_id = place_node[:id].to_sym
23
+ initial_marking_nodes = place_node.xpath('initialMarking/value/text()')
24
+ initial_marking = initial_marking_nodes.empty? ? 0 : initial_marking_nodes[0].text.split(',')[1].to_i
25
+
26
+ # place = Place.new(initial_marking)
27
+ # place_by_id[place_id] = place
28
+
29
+ # TODO: Remove
30
+ markings[place_id] = initial_marking
31
+ end
32
+
33
+ doc.xpath('//transition').each do |transition_node|
34
+ transition_id = transition_node[:id].to_sym
35
+
36
+ # transition = Transition.new
37
+ # transition_by_id[transition_id] = transition
38
+
39
+ # TODO: Remove
40
+ transitions[transition_id]
41
+ end
42
+
43
+ doc.xpath('//arc').each do |arc_node|
44
+ source_id = arc_node[:source].to_sym
45
+ target_id = arc_node[:target].to_sym
46
+
47
+ # edge = Edge.new(source_id, target_id)
48
+ # edges.push(edge)
49
+
50
+ if transitions.has_key?(source_id)
51
+ transition_name = source_id
52
+ place_name = target_id
53
+ transitions[transition_name][:give] << place_name
54
+ elsif transitions.has_key?(target_id)
55
+ transition_name = target_id
56
+ place_name = source_id
57
+ transitions[transition_name][:take] << place_name
58
+ else
59
+ raise "Unknown transition name in one of: #{source_id} -> #{target_id}"
60
+ end
61
+ end
62
+
63
+ net = Net.build do |b|
64
+ transitions.each do |transition_name, places|
65
+ b.transition(transition_name, take: places[:take], give: places[:give])
66
+ end
67
+ end
68
+ net.mark(markings)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,3 @@
1
+ module Petrinet
2
+ VERSION = "0.1.0"
3
+ end
data/lib/petrinet.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "petrinet/version"
2
+ require "petrinet/net"
3
+ require "petrinet/marking_transition_script"
4
+
5
+ module Petrinet
6
+ class Error < StandardError; end
7
+ # Your code goes here...
8
+ end
data/petrinet.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "petrinet/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "petrinet"
7
+ spec.version = Petrinet::VERSION
8
+ spec.authors = ["Aslak Hellesøy"]
9
+ spec.email = ["aslak.hellesoy@smartbear.com"]
10
+
11
+ spec.summary = %q{Petri Net simulator}
12
+ spec.description = spec.description
13
+ spec.homepage = "https://github.com/cucumber/petrinet"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+
18
+ spec.metadata["homepage_uri"] = spec.homepage
19
+ spec.metadata["source_code_uri"] = "#{spec.homepage}.git"
20
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
25
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ end
27
+ spec.bindir = "exe"
28
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.require_paths = ["lib"]
30
+
31
+ spec.add_dependency "nokogiri", "~> 1.10.4"
32
+
33
+ spec.add_development_dependency "bundler", "~> 2.0"
34
+ spec.add_development_dependency "rake", "~> 10.0"
35
+ spec.add_development_dependency "rspec", "~> 3.0"
36
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: petrinet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Aslak Hellesøy
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-10-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: nokogiri
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.10.4
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.10.4
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: ''
70
+ email:
71
+ - aslak.hellesoy@smartbear.com
72
+ executables:
73
+ - petrinet
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - Gemfile
81
+ - LICENSE.txt
82
+ - Makefile
83
+ - README.md
84
+ - Rakefile
85
+ - examples/cucumber-protocol/cucumber-protocol.xml
86
+ - examples/cucumber-protocol/transition_sample_1.gif
87
+ - examples/cucumber-protocol/transition_sample_1.txt
88
+ - examples/voting/voting.xml
89
+ - examples/x-ray-machine/v1-problem.gif
90
+ - examples/x-ray-machine/v1-problem.txt
91
+ - examples/x-ray-machine/v2-fixed.gif
92
+ - examples/x-ray-machine/v2-fixed.txt
93
+ - examples/x-ray-machine/x-ray-machine-1.xml
94
+ - examples/x-ray-machine/x-ray-machine-2.xml
95
+ - exe/petrinet
96
+ - lib/petrinet.rb
97
+ - lib/petrinet/animated_gif_builder.rb
98
+ - lib/petrinet/graphviz_builder.rb
99
+ - lib/petrinet/marking_transition_script.rb
100
+ - lib/petrinet/net.rb
101
+ - lib/petrinet/pnml_builder.rb
102
+ - lib/petrinet/version.rb
103
+ - petrinet.gemspec
104
+ homepage: https://github.com/cucumber/petrinet
105
+ licenses:
106
+ - MIT
107
+ metadata:
108
+ allowed_push_host: https://rubygems.org
109
+ homepage_uri: https://github.com/cucumber/petrinet
110
+ source_code_uri: https://github.com/cucumber/petrinet.git
111
+ changelog_uri: https://github.com/cucumber/petrinet/blob/master/CHANGELOG.md
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.0.3
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Petri Net simulator
131
+ test_files: []