automaton 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README ADDED
@@ -0,0 +1,11 @@
1
+
2
+ Automaton is a simple pure Ruby implementation of automata. It makes it easy to create, modify and visualize utomata. See also http://www.jklm.no/automaton.
3
+
4
+ ===== Dependencies
5
+ Automaton depends on the rtex gem, which is used to generate latex for illustrations. To generate images, a working Latex installation is also required.
6
+
7
+ ===== Origin
8
+ Automaton was written as a part of my final year project which used automata to model the semantics of a programming language.
9
+
10
+ ===== License
11
+ Automaton is licenced under the MIT-license. See LICENSE.
data/bin/tex2pdf ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ if ARGV.empty?
3
+ puts "Usage: tex2pdf tex-file.tex"
4
+ exit
5
+ end
6
+
7
+ file = File.basename(ARGV.last, '.tex')
8
+ `latex --interaction=batchmode #{file}`
9
+ `dvipdf #{file}`
data/lib/automaton.rb ADDED
@@ -0,0 +1,243 @@
1
+ require 'ruby_extensions'
2
+ require 'latex'
3
+ require 'layout'
4
+ require 'set'
5
+
6
+ # Author:: Tor Erik Linnerud (tel@jklm.no)
7
+ # Author:: Tom Gundersen (teg@jklm.no)
8
+ # Copyright:: Copyright (c) 2008 JKLM DA
9
+ # License:: MIT
10
+ class Automaton
11
+
12
+ attr_reader :start, :finals, :graph, :alphabet
13
+
14
+
15
+ # Create a new automaton. new is intended for internal use.
16
+ # create makes it easier to create an automaton from scratch.
17
+ # start - A symbol
18
+ # finals - A set of symbols
19
+ # graph - A transition function (Graph) which can be created like this:
20
+ # Automaton.new(:a, Set[:c], Graph.from_hash(:a => {'1' => Set[:b]}, :b => {'2' => Set[:a, :c]}))
21
+ # This is interpreted as an Automaton with start state a, final (accepting) states :c,
22
+ # a transition from :a to :b on the letter 1 and a transition from :b to :a and :c on the letter 2.
23
+ def initialize(start, finals, graph)
24
+ raise ArgumentError, 'Invalid transition function' unless graph.is_a?(Graph)
25
+ @start = start
26
+ @finals = finals
27
+ @graph = graph
28
+ @alphabet = graph.values.map{|transitions| transitions.keys}.flatten.to_set
29
+ end
30
+
31
+ # Create a new automaton, intended for public use
32
+ # Unlike new, create allows you to use a single element instead of an array when you just have a single element. Furthermore,
33
+ # graph can be (and must be) a hash, instead of a Graph.
34
+ # Instead of
35
+ # Automaton.new(:a, Set[:c], Graph.from_hash(:a => {'1' => [:b]}, :b => {'2' => [:a, :c]}))
36
+ # you can now simply do
37
+ # Automaton.create(:a, :c, :a => {'1' => :b}, :b => {'2' => :c})
38
+ def self.create(start, finals, graph)
39
+ raise ArgumentError, "finals shouldn't be passed to create as a set" if finals.is_a?(Set)
40
+ nfa_graph = graph.value_map do |state, transitions|
41
+ transitions.value_map {|symbol, s| [s].flatten}
42
+ end
43
+ self.new(start, [finals].flatten.to_set, Graph.from_hash(nfa_graph)).prune
44
+ end
45
+
46
+ # Keep only reachable states. (Removes all unreachable states.)
47
+ def prune
48
+ reachable_states_cache = reachable_states
49
+ finals = self.finals & reachable_states_cache
50
+ graph = self.graph.prune(reachable_states_cache)
51
+ self.class.new(start, finals, graph)
52
+ end
53
+
54
+ # Automaton accepting the complement of the language accepted by self
55
+ def complement
56
+ self.class.new(start, reachable_states - finals, graph)
57
+ end
58
+
59
+ # States reachable from the start state (default), or any other given state
60
+ def reachable_states(from = start, already_seen = Set.new)
61
+ new_states = (successors_of(from) - already_seen - [from])
62
+ already_seen = already_seen + new_states + [from]
63
+ new_states.inject(Set.new){|reachables, state| reachables + reachable_states(state, already_seen)} + [from]
64
+ end
65
+
66
+ # States reachable from state in one sucession
67
+ def successors_of(state)
68
+ state_transitions = graph[state]
69
+ return [] unless state_transitions
70
+ state_transitions.values.inject(Set.new){|reachables, states| reachables + states}
71
+ end
72
+
73
+ # Will the Automaton accept any string at all?
74
+ def accepting?
75
+ !(reachable_states & finals).empty?
76
+ end
77
+
78
+ # New automaton with each state tagged with the given name
79
+ def tag(name)
80
+ tagged_finals = finals.map{|state| state.tag(name)}.to_set
81
+ tagged_graph = graph.key_value_map do |state, transitions|
82
+ tagged_transitions = transitions.value_map do |symbol, states|
83
+ states.map{|s| s.tag(name)}.to_set
84
+ end
85
+ [state.tag(name), tagged_transitions]
86
+ end
87
+ self.class.new(start.tag(name), tagged_finals, tagged_graph)
88
+ end
89
+
90
+ def ==(other)
91
+ start == other.start &&
92
+ finals == other.finals &&
93
+ graph == other.graph
94
+ end
95
+
96
+ # Create the intersection of two automata, which is basically the cartesian product of the two
97
+ def intersect(other)
98
+ start = self.start + other.start
99
+ finals = self.finals.to_a.product(other.finals.to_a).map{|a,b| a + b}.to_set
100
+ product = self.graph.product(other.graph)
101
+ graph = product.key_value_map do |(state1, state2), (transitions1, transitions2)|
102
+ common_symbols = transitions1.keys & transitions2.keys
103
+ transitions = common_symbols.map do |symbol|
104
+ states = transitions1[symbol].to_a.product(transitions2[symbol].to_a).map{|a, b| a + b}
105
+ [symbol, states]
106
+ end.to_h
107
+ [state1 + state2, transitions]
108
+ end
109
+ self.class.new(start, finals, graph).prune
110
+ end
111
+
112
+ # Automaton accepting the language accepted by self minus the language accepted by other
113
+ def -(other)
114
+ alphabet = self.alphabet + other.alphabet
115
+ self.intersect(other.to_total(alphabet).complement)
116
+ end
117
+
118
+ # self.language subset of other.language?
119
+ def subset?(other)
120
+ !(self - other).accepting?
121
+ end
122
+
123
+ # self.language == other.language?
124
+ def accepting_same_language_as?(other)
125
+ self.subset?(other) &&
126
+ other.subset?(self)
127
+ end
128
+
129
+ # Set of transitions in the Automaton
130
+ def transitions
131
+ graph.map do |from, transitions|
132
+ transitions.map do |label, to|
133
+ to.map do |to|
134
+ Transition.new(from, to, label)
135
+ end
136
+ end
137
+ end.flatten
138
+ end
139
+
140
+ # New automaton which is the total version of self. This means that all states have a transition for every symbol in the alphabet.
141
+ def to_total(alphabet)
142
+ raise ArgumentError unless alphabet.is_a?(Set)
143
+ all_states = Graph.from_hash((reachable_states + [:x]).map{|state| [state, {}]}.to_h)
144
+ total_graph = all_states.merge(graph).value_map do |state, transitions|
145
+ missing_symbols = (alphabet - transitions.keys.to_set)
146
+ missing_transitions = missing_symbols.map{|symbol| [symbol, [:x]]}.to_h
147
+ transitions.merge(missing_transitions)
148
+ end
149
+ self.class.new(start, finals, total_graph).prune
150
+ end
151
+
152
+ # Image of the Automaton in the form of a string of latex
153
+ def to_tex
154
+ nodes = reachable_states.each_with_index.map{|state, i| [state, Layout::Node.new(state, rand * i , rand * i)]}.to_h
155
+ transitions.each do |transition|
156
+ nodes[transition.from].connect(nodes[transition.to])
157
+ end
158
+ nodes.values.each do |node|
159
+ node.position *= 2.5
160
+ end
161
+ layout = Layout.new(*nodes.values)
162
+ layout.force_direct
163
+ layout.normalize
164
+ Latex.new(start, finals, nodes.values, transitions)
165
+ end
166
+
167
+ Transition = Struct.new(:from, :to, :label)
168
+
169
+
170
+ class Graph < Hash
171
+
172
+ # Create from hash
173
+ def self.from_hash(hash)
174
+ # Change from storing the states as an array to a set
175
+ self.new.replace(hash.value_map do |state,transitions|
176
+ transitions.value_map do |symbol,states|
177
+ states.to_set
178
+ end
179
+ end)
180
+ end
181
+
182
+ # Creates a new hash where the new keys are the cartesian product of
183
+ # the keys of the old hashes and the new values the pair of values created by
184
+ # self.values_at(new_key.first), other.values_at(new_key.last)
185
+ # {:a => 1, :b => 2}.product(:c => 3, :d => 4)
186
+ # #=> {[:a, :d] => [1, 4], [:a, :c] => [1, 3], [:b, :c] => [2, 3], [:b, :d] => [2, 4]}
187
+ def product(other)
188
+ self.keys.product(other.keys).inject(self.class.new) do |hash, (key1, key2)|
189
+ hash[[key1,key2]] = [self[key1],other[key2]]
190
+ hash
191
+ end
192
+ end
193
+
194
+ # Invokes block once for each element of self, each time yielding key and value.
195
+ # Creates a new hash from the key => value pairs returned by the block,
196
+ # these pairs should be an array of the form [key, value].
197
+ #
198
+ # {:a => 1, :b => 2}.key_value_map{|key, value| [key.to_s, value * 2]}
199
+ # #=> {"a" => 2, "b" => 4}
200
+ def key_value_map
201
+ self.inject(self.class.new) do |hash, (key,value)|
202
+ new_key, new_value = yield(key, value)
203
+ hash[new_key] = new_value
204
+ hash
205
+ end
206
+ end
207
+
208
+ def [](state)
209
+ return super || {}
210
+ end
211
+
212
+ def to_hash
213
+ Hash.new.replace(self)
214
+ end
215
+
216
+ def merge(other)
217
+ raise ArgumentError, 'Merging with something that is not a TransitionFunction' unless other.is_a?(Graph)
218
+ new = {}
219
+ self.each do |state, transitions|
220
+ new[state] = Graph.merge_transitions(other[state],transitions)
221
+ end
222
+ other.each do |state, transitions|
223
+ new[state] = transitions unless new.has_key?(state)
224
+ end
225
+ Graph.from_hash(new)
226
+ end
227
+
228
+ def self.merge_transitions(t1,t2)
229
+ new = {}
230
+ t1.each do |symbol,states|
231
+ new[symbol] = (states + (t2[symbol] || Set.new))
232
+ end
233
+ t2.merge(new)
234
+ end
235
+
236
+ def prune(reachable_states)
237
+ pruned = self.select do |state,_|
238
+ reachable_states.include? state
239
+ end.to_h
240
+ Graph.from_hash(pruned)
241
+ end
242
+ end
243
+ end
data/lib/generate.rb ADDED
@@ -0,0 +1,43 @@
1
+ require 'node'
2
+
3
+ Node = Automaton::Layout::Node
4
+
5
+ a = Node.new(:a, 0, 0)
6
+ b = Node.new(:b, 1, 0)
7
+ c = Node.new(:c, 2, 0)
8
+ d = Node.new(:d, 2, 2)
9
+
10
+ nodes = [a, b, c, d]
11
+ b.connect(a, c, d)
12
+
13
+ graph = Automaton::Layout.new(*nodes)
14
+ graph.force_direct
15
+ graph.normalize
16
+
17
+
18
+
19
+ states = graph.nodes.map do |node|
20
+ x, y = *(node.position * 2.5)
21
+ "\\State[%s]{(%.2f, %.3f)}{%s}" % [node.name, x, y, node.name.to_s.upcase]
22
+ end
23
+
24
+ transitions = graph.nodes.map do |node|
25
+ node.connections.map{|other| [node.name.to_s, other.name.to_s].sort}
26
+ end.flatten(1).uniq.map do |from, to|
27
+ "\\EdgeL{#{from.upcase}}{#{to.upcase}}{#{from}-#{to}}"
28
+ end
29
+
30
+ initial = a.name.to_s.upcase
31
+ initial = "\\Initial{#{initial}}"
32
+ final = d.name.to_s.upcase
33
+ final = "\\Final{#{final}}"
34
+
35
+ puts transitions.inspect
36
+
37
+ string = states.join(' ')
38
+ source = File.read('template.tex')
39
+ source.sub!('STATES', states.join("\n"))
40
+ source.sub!('TRANSITIONS', transitions.join("\n"))
41
+ source.sub!('INITIAL', initial)
42
+ source.sub!('FINAL', final)
43
+ File.open('automaton.tex', 'w'){|file| file.write(source)}
data/lib/latex.rb ADDED
@@ -0,0 +1,61 @@
1
+ # Author:: Tor Erik Linnerud (tel@jklm.no)
2
+ # Copyright:: Copyright (c) 2008 JKLM DA
3
+ # License:: MIT
4
+
5
+ require 'rtex'
6
+ require 'fileutils'
7
+
8
+ class Automaton
9
+ # Latex representation of an Automaton
10
+ class Latex
11
+ def initialize(initial, finals, states, transitions)
12
+ @initial = Initial.new(initial)
13
+ @finals = finals.map{|final| Final.new(final)}
14
+ @states = states.map{|state| State.new(*state.to_a)}
15
+ @transitions = transitions.map{|transition| Transition.new(*transition.to_a)}
16
+ end
17
+
18
+ def render_pdf(file_path)
19
+ target_path = File.expand_path(file_path)
20
+ our_path = File.expand_path(File.dirname(__FILE__))
21
+ ENV['TEXINPUTS'] = ".:#{our_path}/tex:"
22
+ template = File.read("#{our_path}/tex/template.tex")
23
+ doc = RTeX::Document.new(template, :processor => 'tex2pdf')
24
+ doc.to_pdf(binding) do |tempfile_path|
25
+ FileUtils.mv tempfile_path, target_path
26
+ end
27
+ end
28
+
29
+ def values
30
+ [@initial, @finals, @states, @transitions]
31
+ end
32
+
33
+ def ==(other)
34
+ self.values == other.values
35
+ end
36
+
37
+ Initial = Struct.new(:name) do
38
+ def to_s
39
+ "\\Initial{#{name}}"
40
+ end
41
+ end
42
+
43
+ State = Struct.new(:name, :x, :y) do
44
+ def to_s
45
+ "\\State[%s]{(%.2f, %.2f)}{#{name}}" % self.to_a
46
+ end
47
+ end
48
+
49
+ Transition = Struct.new(:from, :to, :label) do
50
+ def to_s
51
+ "\\EdgeL{#{from}}{#{to}}{#{label}}"
52
+ end
53
+ end
54
+
55
+ Final = Struct.new(:name) do
56
+ def to_s
57
+ "\\Final{#{name}}"
58
+ end
59
+ end
60
+ end
61
+ end
data/lib/layout.rb ADDED
@@ -0,0 +1,116 @@
1
+ require 'matrix'
2
+ require 'set'
3
+
4
+ # Author:: Tor Erik Linnerud (tel@jklm.no)
5
+ # Copyright:: Copyright (c) 2008 JKLM DA
6
+ # License:: MIT
7
+
8
+ class Automaton
9
+ # An ad-hoc implmenentation of a spring-force algorithm to layout the automaton graph.
10
+ class Layout
11
+
12
+ SPRING_LENGTH = 3
13
+ REPULSION = 3
14
+ ATTRACTION = 0.40
15
+ TIMESTEP = 0.25
16
+ ENERGY_TRESHOLD = 0.1
17
+ DAMPING = 0.8
18
+
19
+ attr_reader :nodes
20
+ def initialize(*nodes)
21
+ @nodes = nodes
22
+ end
23
+
24
+
25
+ def force_direct
26
+ nodes.each{|node| node.velocity = Vector[0.0, 0.0]}
27
+ total_kinetic_energy = ENERGY_TRESHOLD + 1
28
+ until total_kinetic_energy < ENERGY_TRESHOLD
29
+ total_kinetic_energy = 0.0
30
+ nodes.each do |node|
31
+ net_force = (nodes - [node]).reduce(Vector[0, 0]) do |sum, other_node|
32
+ sum += node.repulsion(other_node) + node.attraction(other_node)
33
+ end
34
+ node.velocity = (node.velocity + net_force * TIMESTEP) * DAMPING
35
+ node.position += node.velocity * TIMESTEP
36
+ total_kinetic_energy += node.speed**2
37
+ end
38
+ end
39
+ end
40
+
41
+ def normalize
42
+ min_x = nodes.map{|node| node.x}.min
43
+ min_y = nodes.map{|node| node.y}.min
44
+ min = Vector[min_x - 1, min_y - 1]
45
+ nodes.each{|node| node.position -= min}
46
+ end
47
+
48
+ def to_s
49
+ nodes.map{|node| node.position}.inspect
50
+ end
51
+
52
+ # A spring connected, repulsing node used by the spring force algorithm
53
+ class Node
54
+ attr_reader :name
55
+ attr_accessor :position, :velocity, :connections
56
+ def initialize(name, x, y)
57
+ @name = name
58
+ @position = Vector[x, y]
59
+ @velocity = Vector[0, 0]
60
+ @connections = Set.new
61
+ end
62
+
63
+ def connect(*nodes)
64
+ @connections = @connections | nodes.to_set
65
+ @connections.each do |node|
66
+ node.connect(self) unless node.connected?(self)
67
+ end
68
+ end
69
+
70
+ def connected?(other)
71
+ @connections.member?(other)
72
+ end
73
+
74
+ def repulsion(other)
75
+ vector = vector(other)
76
+ vector.unit * REPULSION * (1 / (4 * Math::PI * vector.r**2))
77
+ end
78
+
79
+ def attraction(other)
80
+ return Vector[0, 0] unless connected?(other)
81
+ vector = vector(other)
82
+ vector.unit * -(ATTRACTION * (SPRING_LENGTH - vector.r.abs))
83
+ end
84
+
85
+ def vector(other)
86
+ other.position - self.position
87
+ end
88
+
89
+ def speed
90
+ velocity.r
91
+ end
92
+
93
+ def x
94
+ position[0]
95
+ end
96
+
97
+ def y
98
+ position[1]
99
+ end
100
+
101
+ def to_a
102
+ [name, x, y]
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ class Vector #:nodoc:
109
+ def unit
110
+ self * (1 / self.r)
111
+ end
112
+
113
+ def -@
114
+ self.map{|a| -a}
115
+ end
116
+ end