stamina 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/CHANGELOG.md +24 -0
  2. data/Gemfile.lock +5 -1
  3. data/bin/stamina +10 -0
  4. data/lib/stamina.rb +2 -1
  5. data/lib/stamina/abbadingo.rb +2 -0
  6. data/lib/stamina/abbadingo/random_dfa.rb +48 -0
  7. data/lib/stamina/abbadingo/random_sample.rb +146 -0
  8. data/lib/stamina/adl.rb +6 -6
  9. data/lib/stamina/automaton.rb +29 -4
  10. data/lib/stamina/automaton/complete.rb +36 -0
  11. data/lib/stamina/automaton/equivalence.rb +55 -0
  12. data/lib/stamina/automaton/metrics.rb +8 -1
  13. data/lib/stamina/automaton/minimize.rb +25 -0
  14. data/lib/stamina/automaton/minimize/hopcroft.rb +116 -0
  15. data/lib/stamina/automaton/minimize/pitchies.rb +64 -0
  16. data/lib/stamina/automaton/strip.rb +16 -0
  17. data/lib/stamina/automaton/walking.rb +46 -19
  18. data/lib/stamina/command.rb +45 -0
  19. data/lib/stamina/command/abbadingo_dfa.rb +81 -0
  20. data/lib/stamina/command/abbadingo_samples.rb +40 -0
  21. data/lib/stamina/command/adl2dot.rb +71 -0
  22. data/lib/stamina/command/classify.rb +48 -0
  23. data/lib/stamina/command/help.rb +27 -0
  24. data/lib/stamina/command/infer.rb +141 -0
  25. data/lib/stamina/command/metrics.rb +51 -0
  26. data/lib/stamina/command/robustness.rb +22 -0
  27. data/lib/stamina/command/score.rb +35 -0
  28. data/lib/stamina/errors.rb +4 -1
  29. data/lib/stamina/ext/math.rb +20 -0
  30. data/lib/stamina/induction/{redblue.rb → blue_fringe.rb} +29 -28
  31. data/lib/stamina/induction/commons.rb +32 -46
  32. data/lib/stamina/induction/rpni.rb +7 -9
  33. data/lib/stamina/induction/union_find.rb +3 -3
  34. data/lib/stamina/loader.rb +1 -0
  35. data/lib/stamina/sample.rb +79 -2
  36. data/lib/stamina/scoring.rb +37 -0
  37. data/lib/stamina/version.rb +2 -2
  38. data/stamina.gemspec +2 -1
  39. data/stamina.noespec +9 -12
  40. data/test/stamina/abbadingo/random_dfa_test.rb +16 -0
  41. data/test/stamina/abbadingo/random_sample_test.rb +78 -0
  42. data/test/stamina/adl_test.rb +27 -2
  43. data/test/stamina/automaton/complete_test.rb +58 -0
  44. data/test/stamina/automaton/equivalence_test.rb +120 -0
  45. data/test/stamina/automaton/minimize/hopcroft_test.rb +15 -0
  46. data/test/stamina/automaton/minimize/minimize_test.rb +55 -0
  47. data/test/stamina/automaton/minimize/pitchies_test.rb +15 -0
  48. data/test/stamina/automaton/minimize/rice_edu_10.adl +16 -0
  49. data/test/stamina/automaton/minimize/rice_edu_10.min.adl +13 -0
  50. data/test/stamina/automaton/minimize/rice_edu_13.adl +13 -0
  51. data/test/stamina/automaton/minimize/rice_edu_13.min.adl +7 -0
  52. data/test/stamina/automaton/minimize/should_strip_1.adl +8 -0
  53. data/test/stamina/automaton/minimize/should_strip_1.min.adl +6 -0
  54. data/test/stamina/automaton/minimize/unknown_1.adl +16 -0
  55. data/test/stamina/automaton/minimize/unknown_1.min.adl +12 -0
  56. data/test/stamina/automaton/strip_test.rb +36 -0
  57. data/test/stamina/automaton/walking/dfa_delta_test.rb +39 -0
  58. data/test/stamina/automaton_test.rb +13 -1
  59. data/test/stamina/induction/{redblue_test.rb → blue_fringe_test.rb} +22 -22
  60. data/test/stamina/sample_test.rb +75 -0
  61. data/test/stamina/stamina_test.rb +13 -2
  62. metadata +98 -23
  63. data/bin/adl2dot +0 -12
  64. data/bin/classify +0 -12
  65. data/bin/redblue +0 -12
  66. data/bin/rpni +0 -12
  67. data/lib/stamina/command/adl2dot_command.rb +0 -73
  68. data/lib/stamina/command/classify_command.rb +0 -57
  69. data/lib/stamina/command/redblue_command.rb +0 -58
  70. data/lib/stamina/command/rpni_command.rb +0 -58
  71. 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{|s0,s1| s0[:depth] <=> s1[:depth]}[:depth]
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
- delta = walking_to_from(from).collect{|s| s.dfa_delta(symbol)}.flatten.uniq
172
- walking_to_dfa_result(delta, from)
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
- # the three elements of the triplet
223
- parsed = []
224
- reached = walking_to_from(from)
225
- remaining = walking_to_modifiable_symbols(input)
226
-
227
- # walk now
228
- until remaining.empty?
229
- symb = remaining[0]
230
- next_reached = dfa_delta(reached, symb)
231
-
232
- # stop it if no reached state
233
- break if next_reached.nil? or next_reached.empty?
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
- # otherwise, update triplet
236
- parsed << remaining.shift
237
- reached = next_reached
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
+