automaton 0.0.1

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.
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