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