automaton 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +11 -0
- data/bin/tex2pdf +9 -0
- data/lib/automaton.rb +243 -0
- data/lib/generate.rb +43 -0
- data/lib/latex.rb +61 -0
- data/lib/layout.rb +116 -0
- data/lib/ruby_extensions.rb +35 -0
- data/lib/tex/FinalStateVar.tex +14 -0
- data/lib/tex/VCPref-main.tex +109 -0
- data/lib/tex/Vaucanson-G.tex +946 -0
- data/lib/tex/multido.sty +27 -0
- data/lib/tex/template.tex +24 -0
- data/lib/tex/vaucanson-g.sty +14 -0
- data/spec/automaton_spec.rb +180 -0
- data/spec/layout_spec.rb +48 -0
- data/spec/spec.opts +0 -0
- data/spec/spec_helper.rb +45 -0
- metadata +78 -0
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
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
|