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