stamina 0.3.1 → 0.4.0
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/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
|
+
|