stamina 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +5 -1
- data/bin/stamina +10 -0
- data/lib/stamina.rb +2 -1
- data/lib/stamina/abbadingo.rb +2 -0
- data/lib/stamina/abbadingo/random_dfa.rb +48 -0
- data/lib/stamina/abbadingo/random_sample.rb +146 -0
- data/lib/stamina/adl.rb +6 -6
- data/lib/stamina/automaton.rb +29 -4
- data/lib/stamina/automaton/complete.rb +36 -0
- data/lib/stamina/automaton/equivalence.rb +55 -0
- data/lib/stamina/automaton/metrics.rb +8 -1
- data/lib/stamina/automaton/minimize.rb +25 -0
- data/lib/stamina/automaton/minimize/hopcroft.rb +116 -0
- data/lib/stamina/automaton/minimize/pitchies.rb +64 -0
- data/lib/stamina/automaton/strip.rb +16 -0
- data/lib/stamina/automaton/walking.rb +46 -19
- data/lib/stamina/command.rb +45 -0
- data/lib/stamina/command/abbadingo_dfa.rb +81 -0
- data/lib/stamina/command/abbadingo_samples.rb +40 -0
- data/lib/stamina/command/adl2dot.rb +71 -0
- data/lib/stamina/command/classify.rb +48 -0
- data/lib/stamina/command/help.rb +27 -0
- data/lib/stamina/command/infer.rb +141 -0
- data/lib/stamina/command/metrics.rb +51 -0
- data/lib/stamina/command/robustness.rb +22 -0
- data/lib/stamina/command/score.rb +35 -0
- data/lib/stamina/errors.rb +4 -1
- data/lib/stamina/ext/math.rb +20 -0
- data/lib/stamina/induction/{redblue.rb → blue_fringe.rb} +29 -28
- data/lib/stamina/induction/commons.rb +32 -46
- data/lib/stamina/induction/rpni.rb +7 -9
- data/lib/stamina/induction/union_find.rb +3 -3
- data/lib/stamina/loader.rb +1 -0
- data/lib/stamina/sample.rb +79 -2
- data/lib/stamina/scoring.rb +37 -0
- data/lib/stamina/version.rb +2 -2
- data/stamina.gemspec +2 -1
- data/stamina.noespec +9 -12
- data/test/stamina/abbadingo/random_dfa_test.rb +16 -0
- data/test/stamina/abbadingo/random_sample_test.rb +78 -0
- data/test/stamina/adl_test.rb +27 -2
- data/test/stamina/automaton/complete_test.rb +58 -0
- data/test/stamina/automaton/equivalence_test.rb +120 -0
- data/test/stamina/automaton/minimize/hopcroft_test.rb +15 -0
- data/test/stamina/automaton/minimize/minimize_test.rb +55 -0
- data/test/stamina/automaton/minimize/pitchies_test.rb +15 -0
- data/test/stamina/automaton/minimize/rice_edu_10.adl +16 -0
- data/test/stamina/automaton/minimize/rice_edu_10.min.adl +13 -0
- data/test/stamina/automaton/minimize/rice_edu_13.adl +13 -0
- data/test/stamina/automaton/minimize/rice_edu_13.min.adl +7 -0
- data/test/stamina/automaton/minimize/should_strip_1.adl +8 -0
- data/test/stamina/automaton/minimize/should_strip_1.min.adl +6 -0
- data/test/stamina/automaton/minimize/unknown_1.adl +16 -0
- data/test/stamina/automaton/minimize/unknown_1.min.adl +12 -0
- data/test/stamina/automaton/strip_test.rb +36 -0
- data/test/stamina/automaton/walking/dfa_delta_test.rb +39 -0
- data/test/stamina/automaton_test.rb +13 -1
- data/test/stamina/induction/{redblue_test.rb → blue_fringe_test.rb} +22 -22
- data/test/stamina/sample_test.rb +75 -0
- data/test/stamina/stamina_test.rb +13 -2
- metadata +98 -23
- data/bin/adl2dot +0 -12
- data/bin/classify +0 -12
- data/bin/redblue +0 -12
- data/bin/rpni +0 -12
- data/lib/stamina/command/adl2dot_command.rb +0 -73
- data/lib/stamina/command/classify_command.rb +0 -57
- data/lib/stamina/command/redblue_command.rb +0 -58
- data/lib/stamina/command/rpni_command.rb +0 -58
- data/lib/stamina/command/stamina_command.rb +0 -79
@@ -52,6 +52,8 @@ module Stamina
|
|
52
52
|
def depth(key = :depth)
|
53
53
|
algo = Stamina::Utils::Decorate.new(key)
|
54
54
|
algo.set_suppremum do |d0,d1|
|
55
|
+
# Here, unreached state has the max value (i.e. nil is +INF)
|
56
|
+
# and we look at the minimum depth for each state
|
55
57
|
if d0.nil?
|
56
58
|
d1
|
57
59
|
elsif d1.nil?
|
@@ -62,7 +64,12 @@ module Stamina
|
|
62
64
|
end
|
63
65
|
algo.set_propagate {|d,e| d+1 }
|
64
66
|
algo.execute(self, nil, 0)
|
65
|
-
states.max
|
67
|
+
deepest = states.max do |s0,s1|
|
68
|
+
# Here, we do not take unreachable states into account
|
69
|
+
# so that -1 is taken when nil is encountered
|
70
|
+
(s0[:depth] || -1) <=> (s1[:depth] || -1)
|
71
|
+
end
|
72
|
+
deepest[:depth]
|
66
73
|
end
|
67
74
|
|
68
75
|
end # module Metrics
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Stamina
|
2
|
+
class Automaton
|
3
|
+
|
4
|
+
#
|
5
|
+
# Checks if this automaton is minimal.
|
6
|
+
#
|
7
|
+
def minimal?
|
8
|
+
self.minimize <=> self.complete
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# Returns a minimized version of this automaton.
|
13
|
+
#
|
14
|
+
# This method should only be called on deterministic automata.
|
15
|
+
#
|
16
|
+
def minimize(options = {})
|
17
|
+
Minimize::Hopcroft.execute(self, options)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
end # class Automaton
|
22
|
+
end # module Stamina
|
23
|
+
require 'stamina/automaton/minimize/hopcroft'
|
24
|
+
require 'stamina/automaton/minimize/pitchies'
|
25
|
+
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module Stamina
|
2
|
+
class Automaton
|
3
|
+
module Minimize
|
4
|
+
class Hopcroft
|
5
|
+
|
6
|
+
# Creates an algorithm instance
|
7
|
+
def initialize(automaton, options)
|
8
|
+
raise ArgumentError, "Deterministic automaton expected", caller unless automaton.deterministic?
|
9
|
+
@automaton = automaton
|
10
|
+
end
|
11
|
+
|
12
|
+
# Compute a Hash {symbol => state_group} from a group of states
|
13
|
+
def reverse_delta(group)
|
14
|
+
h = Hash.new{|h,k| h[k]=Set.new}
|
15
|
+
group.each do |state|
|
16
|
+
state.in_edges.each do |edge|
|
17
|
+
h[edge.symbol] << edge.source
|
18
|
+
end
|
19
|
+
end
|
20
|
+
h
|
21
|
+
end
|
22
|
+
|
23
|
+
# Computes a minimal dfa from the grouping information
|
24
|
+
def compute_minimal_dfa(groups)
|
25
|
+
indexes = []
|
26
|
+
Automaton.new do |fa|
|
27
|
+
|
28
|
+
# create one state for each group
|
29
|
+
groups.each_with_index do |group,index|
|
30
|
+
group.each{|s| indexes[s.index] = index}
|
31
|
+
data = group.inject(nil) do |memo,s|
|
32
|
+
if memo.nil?
|
33
|
+
s.data
|
34
|
+
else
|
35
|
+
{:initial => memo[:initial] || s.initial?,
|
36
|
+
:accepting => memo[:accepting] || s.accepting?,
|
37
|
+
:error => memo[:error] || s.error?}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
fa.add_state(data)
|
41
|
+
end
|
42
|
+
|
43
|
+
# connect transitions now
|
44
|
+
groups.each_with_index do |group,index|
|
45
|
+
group.each do |s|
|
46
|
+
s_index = indexes[s.index]
|
47
|
+
s.out_edges.each do |edge|
|
48
|
+
symbol, t_index = edge.symbol, indexes[edge.target.index]
|
49
|
+
unless fa.ith_state(s_index).dfa_step(symbol)
|
50
|
+
fa.connect(s_index, t_index, symbol)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Computes the initial partition
|
60
|
+
def initial_partition
|
61
|
+
p = [Set.new, Set.new]
|
62
|
+
@automaton.states.each do |s|
|
63
|
+
(s.accepting? ? p[0] : p[1]) << s
|
64
|
+
end
|
65
|
+
p.reject{|g| g.empty?}
|
66
|
+
end
|
67
|
+
|
68
|
+
# Main method of the algorithm
|
69
|
+
def main
|
70
|
+
# Partition states a first time according to accepting/non accepting
|
71
|
+
@partition = initial_partition # P in pseudo code
|
72
|
+
@worklist = @partition.dup # W in pseudo code
|
73
|
+
|
74
|
+
# Until a block needs to be refined
|
75
|
+
until @worklist.empty?
|
76
|
+
refined = @worklist.pop
|
77
|
+
|
78
|
+
# We compute the reverse delta on the group and look at the groups
|
79
|
+
rdelta = reverse_delta(refined)
|
80
|
+
rdelta.each_pair do |symbol, sources| # sources is la in pseudo code
|
81
|
+
|
82
|
+
# Find blocks to be refined
|
83
|
+
@partition.dup.each_with_index do |block, index| # block is R in pseudo code
|
84
|
+
next if block.subset?(sources)
|
85
|
+
intersection = block & sources # R1 in pseudo code
|
86
|
+
next if intersection.empty?
|
87
|
+
difference = block - intersection # R2 in pseudo code
|
88
|
+
|
89
|
+
# replace R in P with R1 and R2
|
90
|
+
@partition[index] = intersection
|
91
|
+
@partition << difference
|
92
|
+
|
93
|
+
# Adds the new blocks as to be refined
|
94
|
+
if @worklist.include?(block)
|
95
|
+
@worklist.delete(block)
|
96
|
+
@worklist << intersection << difference
|
97
|
+
else
|
98
|
+
@worklist << (intersection.size <= difference.size ? intersection : difference)
|
99
|
+
end
|
100
|
+
end # @partition.each
|
101
|
+
|
102
|
+
end # rdelta.each_pair
|
103
|
+
end # until @worklist.empty?
|
104
|
+
|
105
|
+
compute_minimal_dfa(@partition)
|
106
|
+
end # def main
|
107
|
+
|
108
|
+
# Execute the minimizer
|
109
|
+
def self.execute(automaton, options={})
|
110
|
+
Hopcroft.new(automaton.strip.complete!, options).main
|
111
|
+
end
|
112
|
+
|
113
|
+
end # class Hopcroft
|
114
|
+
end # module Minimize
|
115
|
+
end # class Automaton
|
116
|
+
end # module Stamina
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Stamina
|
2
|
+
class Automaton
|
3
|
+
module Minimize
|
4
|
+
class Pitchies
|
5
|
+
|
6
|
+
# Creates an algorithm instance
|
7
|
+
def initialize(automaton, options)
|
8
|
+
raise ArgumentError, "Deterministic automaton expected", caller unless automaton.deterministic?
|
9
|
+
@automaton = automaton
|
10
|
+
end
|
11
|
+
|
12
|
+
def minimized_dfa(oldfa, nb_states, partition)
|
13
|
+
Automaton.new(false) do |newfa|
|
14
|
+
# Add the number of states, with default marks
|
15
|
+
newfa.add_n_states(nb_states, {:initial => false, :accepting => false, :error => false})
|
16
|
+
|
17
|
+
# Refine the marks using the source dfa as reference
|
18
|
+
partition.each_with_index do |block, state_index|
|
19
|
+
source = oldfa.ith_state(state_index)
|
20
|
+
target = newfa.ith_state(block)
|
21
|
+
target.initial! if source.initial?
|
22
|
+
target.accepting! if source.accepting?
|
23
|
+
target.error! if source.error?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Now, create the transitions
|
27
|
+
partition.each_with_index do |block, state_index|
|
28
|
+
source = oldfa.ith_state(state_index)
|
29
|
+
target = newfa.ith_state(block)
|
30
|
+
source.out_edges.each do |edge|
|
31
|
+
where = partition[edge.target.index]
|
32
|
+
if target.dfa_step(edge.symbol) == nil
|
33
|
+
newfa.connect(target, where, edge.symbol)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def main
|
41
|
+
alph, states = @automaton.alphabet, @automaton.states
|
42
|
+
old_nb_states = -1
|
43
|
+
partition = states.collect{|s| s.accepting? ? 1 : 0}
|
44
|
+
until (nb_states = partition.uniq.size) == old_nb_states
|
45
|
+
old_nb_states = nb_states
|
46
|
+
alph.each do |symbol|
|
47
|
+
reached = states.collect{|s| partition[s.dfa_step(symbol).index]}
|
48
|
+
rehash = Hash.new{|h,k| h[k] = h.size}
|
49
|
+
partition = partition.zip(reached).collect{|pair| rehash[pair]}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
minimized_dfa(@automaton, nb_states, partition)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Execute the minimizer
|
56
|
+
def self.execute(automaton, options={})
|
57
|
+
Pitchies.new(automaton.strip.complete!, options).main
|
58
|
+
end
|
59
|
+
|
60
|
+
end # class Pitchies
|
61
|
+
end # module Minimize
|
62
|
+
end # class Automaton
|
63
|
+
end # module Stamina
|
64
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Stamina
|
2
|
+
class Automaton
|
3
|
+
|
4
|
+
# Removes unreachable states from the initial ones
|
5
|
+
def strip!
|
6
|
+
depth(:reachable)
|
7
|
+
drop_states(*states.select{|s| s[:reachable].nil?})
|
8
|
+
end
|
9
|
+
|
10
|
+
# Returns a copy of this automaton with unreachable states removed
|
11
|
+
def strip
|
12
|
+
dup.strip!
|
13
|
+
end
|
14
|
+
|
15
|
+
end # class Automaton
|
16
|
+
end # module Stamina
|
@@ -167,9 +167,13 @@ module Stamina
|
|
167
167
|
# _symbol_. Returns nil (or an empty array, according to the same conventions)
|
168
168
|
# if no such state exists.
|
169
169
|
#
|
170
|
-
def dfa_delta(from, symbol)
|
171
|
-
|
172
|
-
|
170
|
+
def dfa_delta(from, symbol)
|
171
|
+
if from.is_a?(Automaton::State)
|
172
|
+
from.dfa_delta(symbol)
|
173
|
+
else
|
174
|
+
delta = walking_to_from(from).collect{|s| s.dfa_delta(symbol)}.flatten.uniq
|
175
|
+
walking_to_dfa_result(delta, from)
|
176
|
+
end
|
173
177
|
end
|
174
178
|
|
175
179
|
#
|
@@ -219,24 +223,47 @@ module Stamina
|
|
219
223
|
|
220
224
|
# Same as split, respecting dfa conventions.
|
221
225
|
def dfa_split(input, from=nil)
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
226
|
+
from = initial_state if from.nil?
|
227
|
+
from = ith_state(from) if from.is_a?(Integer)
|
228
|
+
if from.is_a?(Automaton::State)
|
229
|
+
# the three elements of the triplet
|
230
|
+
parsed = []
|
231
|
+
reached = from
|
232
|
+
remaining = walking_to_modifiable_symbols(input)
|
233
|
+
|
234
|
+
# walk now
|
235
|
+
until remaining.empty?
|
236
|
+
symb = remaining[0]
|
237
|
+
next_reached = reached.dfa_delta(symb)
|
238
|
+
|
239
|
+
# stop it if no reached state
|
240
|
+
break if next_reached.nil?
|
241
|
+
|
242
|
+
# otherwise, update triplet
|
243
|
+
parsed << remaining.shift
|
244
|
+
reached = next_reached
|
245
|
+
end
|
246
|
+
[parsed, reached, remaining]
|
247
|
+
else
|
248
|
+
# the three elements of the triplet
|
249
|
+
parsed = []
|
250
|
+
reached = walking_to_from(from)
|
251
|
+
remaining = walking_to_modifiable_symbols(input)
|
234
252
|
|
235
|
-
#
|
236
|
-
|
237
|
-
|
253
|
+
# walk now
|
254
|
+
until remaining.empty?
|
255
|
+
symb = remaining[0]
|
256
|
+
next_reached = dfa_delta(reached, symb)
|
257
|
+
|
258
|
+
# stop it if no reached state
|
259
|
+
break if next_reached.nil? or next_reached.empty?
|
260
|
+
|
261
|
+
# otherwise, update triplet
|
262
|
+
parsed << remaining.shift
|
263
|
+
reached = next_reached
|
264
|
+
end
|
265
|
+
[parsed, walking_to_dfa_result(reached, from), remaining]
|
238
266
|
end
|
239
|
-
[parsed, walking_to_dfa_result(reached, from), remaining]
|
240
267
|
end
|
241
268
|
|
242
269
|
#
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'stamina'
|
2
|
+
module Stamina
|
3
|
+
#
|
4
|
+
# Stamina - A Ruby Automaton & Induction Toolkit
|
5
|
+
#
|
6
|
+
# SYNOPSIS
|
7
|
+
# #{program_name} [--version] [--help] COMMAND [cmd opts] ARGS...
|
8
|
+
#
|
9
|
+
# OPTIONS
|
10
|
+
# #{summarized_options}
|
11
|
+
#
|
12
|
+
# COMMANDS
|
13
|
+
# #{summarized_subcommands}
|
14
|
+
#
|
15
|
+
# See '#{program_name} help COMMAND' for more information on a specific command.
|
16
|
+
#
|
17
|
+
class Command < ::Quickl::Delegator(__FILE__, __LINE__)
|
18
|
+
|
19
|
+
# Install options
|
20
|
+
options do |opt|
|
21
|
+
|
22
|
+
# Show the help and exit
|
23
|
+
opt.on_tail("--help", "Show help") do
|
24
|
+
raise Quickl::Help
|
25
|
+
end
|
26
|
+
|
27
|
+
# Show version and exit
|
28
|
+
opt.on_tail("--version", "Show version") do
|
29
|
+
raise Quickl::Exit, "#{program_name} #{VERSION} (c) 2010-2011, Bernard Lambeau"
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end # class Command
|
35
|
+
end # module Stamina
|
36
|
+
require 'stamina/command/robustness'
|
37
|
+
require 'stamina/command/help'
|
38
|
+
require 'stamina/command/adl2dot'
|
39
|
+
require 'stamina/command/metrics'
|
40
|
+
require 'stamina/command/classify'
|
41
|
+
require 'stamina/command/score'
|
42
|
+
require 'stamina/command/abbadingo_dfa'
|
43
|
+
require 'stamina/command/abbadingo_samples'
|
44
|
+
require 'stamina/command/infer'
|
45
|
+
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Stamina
|
2
|
+
class Command
|
3
|
+
#
|
4
|
+
# Generates a DFA following Abbadingo's protocol
|
5
|
+
#
|
6
|
+
# SYNOPSIS
|
7
|
+
# #{program_name} #{command_name}
|
8
|
+
#
|
9
|
+
# OPTIONS
|
10
|
+
# #{summarized_options}
|
11
|
+
#
|
12
|
+
class AbbadingoDfa < Quickl::Command(__FILE__, __LINE__)
|
13
|
+
include Robustness
|
14
|
+
|
15
|
+
# Size of the target automaton
|
16
|
+
attr_accessor :size
|
17
|
+
|
18
|
+
# Tolerance on the size
|
19
|
+
attr_accessor :size_tolerance
|
20
|
+
|
21
|
+
# Tolerance on the automaton depth
|
22
|
+
attr_accessor :depth_tolerance
|
23
|
+
|
24
|
+
# Where to flush the dfa
|
25
|
+
attr_accessor :output_file
|
26
|
+
|
27
|
+
# Install options
|
28
|
+
options do |opt|
|
29
|
+
|
30
|
+
@size = 64
|
31
|
+
opt.on("--size=X", Integer, "Sets the size of the automaton to generate") do |x|
|
32
|
+
@size = x
|
33
|
+
end
|
34
|
+
|
35
|
+
@size_tolerance = nil
|
36
|
+
opt.on("--size-tolerance[=X]", Integer, "Sets the tolerance on automaton size (in number of states)") do |x|
|
37
|
+
@size_tolerance = x
|
38
|
+
end
|
39
|
+
|
40
|
+
@depth_tolerance = 0
|
41
|
+
opt.on("--depth-tolerance[=X]", Integer, "Sets the tolerance on expected automaton depth (in length, 0 by default)") do |x|
|
42
|
+
@depth_tolerance = x
|
43
|
+
end
|
44
|
+
|
45
|
+
@output_file = nil
|
46
|
+
opt.on("-o", "--output=OUTPUT",
|
47
|
+
"Flush DFA in output file") do |value|
|
48
|
+
@output_file = assert_writable_file(value)
|
49
|
+
end
|
50
|
+
|
51
|
+
end # options
|
52
|
+
|
53
|
+
def accept?(dfa)
|
54
|
+
(size_tolerance.nil? || (size - dfa.state_count).abs <= size_tolerance) &&
|
55
|
+
(depth_tolerance.nil? || ((2*Math.log2(size)-2) - dfa.depth).abs <= depth_tolerance)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Command execution
|
59
|
+
def execute(args)
|
60
|
+
require 'stamina/abbadingo'
|
61
|
+
|
62
|
+
# generate it
|
63
|
+
randomizer = Stamina::Abbadingo::RandomDFA.new(size)
|
64
|
+
begin
|
65
|
+
dfa = randomizer.execute
|
66
|
+
end until accept?(dfa)
|
67
|
+
|
68
|
+
# flush it
|
69
|
+
if output_file
|
70
|
+
File.open(output_file, 'w') do |file|
|
71
|
+
Stamina::ADL.print_automaton(dfa, file)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
Stamina::ADL.print_automaton(dfa, $stdout)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end # class AbbadingoDFA
|
79
|
+
end # class Command
|
80
|
+
end # module Stamina
|
81
|
+
|