stamina-core 0.5.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 (36) hide show
  1. data/CHANGELOG.md +78 -0
  2. data/LICENCE.md +22 -0
  3. data/lib/stamina-core/stamina-core.rb +1 -0
  4. data/lib/stamina-core/stamina/adl.rb +298 -0
  5. data/lib/stamina-core/stamina/automaton.rb +1300 -0
  6. data/lib/stamina-core/stamina/automaton/complement.rb +26 -0
  7. data/lib/stamina-core/stamina/automaton/complete.rb +36 -0
  8. data/lib/stamina-core/stamina/automaton/compose.rb +111 -0
  9. data/lib/stamina-core/stamina/automaton/determinize.rb +104 -0
  10. data/lib/stamina-core/stamina/automaton/equivalence.rb +57 -0
  11. data/lib/stamina-core/stamina/automaton/hide.rb +41 -0
  12. data/lib/stamina-core/stamina/automaton/metrics.rb +77 -0
  13. data/lib/stamina-core/stamina/automaton/minimize.rb +23 -0
  14. data/lib/stamina-core/stamina/automaton/minimize/hopcroft.rb +118 -0
  15. data/lib/stamina-core/stamina/automaton/minimize/pitchies.rb +130 -0
  16. data/lib/stamina-core/stamina/automaton/strip.rb +16 -0
  17. data/lib/stamina-core/stamina/automaton/walking.rb +361 -0
  18. data/lib/stamina-core/stamina/command.rb +38 -0
  19. data/lib/stamina-core/stamina/command/adl2dot.rb +82 -0
  20. data/lib/stamina-core/stamina/command/help.rb +23 -0
  21. data/lib/stamina-core/stamina/command/robustness.rb +21 -0
  22. data/lib/stamina-core/stamina/command/run.rb +84 -0
  23. data/lib/stamina-core/stamina/core.rb +11 -0
  24. data/lib/stamina-core/stamina/dsl.rb +6 -0
  25. data/lib/stamina-core/stamina/dsl/automata.rb +23 -0
  26. data/lib/stamina-core/stamina/dsl/core.rb +14 -0
  27. data/lib/stamina-core/stamina/engine.rb +32 -0
  28. data/lib/stamina-core/stamina/engine/context.rb +35 -0
  29. data/lib/stamina-core/stamina/errors.rb +26 -0
  30. data/lib/stamina-core/stamina/ext/math.rb +19 -0
  31. data/lib/stamina-core/stamina/loader.rb +3 -0
  32. data/lib/stamina-core/stamina/markable.rb +42 -0
  33. data/lib/stamina-core/stamina/utils.rb +1 -0
  34. data/lib/stamina-core/stamina/utils/decorate.rb +81 -0
  35. data/lib/stamina-core/stamina/version.rb +14 -0
  36. metadata +93 -0
@@ -0,0 +1,26 @@
1
+ module Stamina
2
+ class Automaton
3
+
4
+ #
5
+ # Returns the complement automaton.
6
+ #
7
+ # A complement automaton is simply a complete automaton with all state
8
+ # labels flipped.
9
+ #
10
+ def complement
11
+ dup.complement!
12
+ end
13
+
14
+ #
15
+ # Complements this automaton
16
+ #
17
+ def complement!
18
+ complete!
19
+ each_state do |s|
20
+ s[:accepting] = !s.accepting?
21
+ end
22
+ self
23
+ end
24
+
25
+ end # class Automaton
26
+ end # module Stamina
@@ -0,0 +1,36 @@
1
+ module Stamina
2
+ class Automaton
3
+
4
+ #
5
+ # Checks if this automaton is complete
6
+ #
7
+ def complete?
8
+ alph = alphabet
9
+ states.find{|s| !(alphabet - s.out_symbols).empty?}.nil?
10
+ end
11
+
12
+ #
13
+ # Returns a completed copy of this automaton
14
+ #
15
+ def complete
16
+ self.dup.complete!
17
+ end
18
+
19
+ #
20
+ # Completes this automaton.
21
+ #
22
+ def complete!(sink_data = {:initial => false, :accepting => false, :error => false})
23
+ alph = alphabet
24
+ sink = add_state(sink_data)
25
+ each_state do |s|
26
+ out_symbols = s.out_symbols
27
+ (alph-out_symbols).each do |symbol|
28
+ connect(s, sink, symbol)
29
+ end
30
+ end
31
+ drop_state(sink) if sink.adjacent_states == [sink]
32
+ self
33
+ end
34
+
35
+ end # class Automaton
36
+ end # module Stamina
@@ -0,0 +1,111 @@
1
+ module Stamina
2
+ class Automaton
3
+ class Compose
4
+
5
+ # Automata under composition
6
+ attr_reader :automata
7
+
8
+ def initialize(automata)
9
+ @automata = automata.collect{|a|
10
+ a.deterministic? ? a : a.determinize
11
+ }
12
+ end
13
+
14
+ def marks(compound, initial = false)
15
+ {:initial => initial,
16
+ :accepting => compound.all?{|s| s.accepting?},
17
+ :error => compound.any?{|s| s.error?}
18
+ }
19
+ end
20
+
21
+ def main
22
+ # Map every symbol to concerned automata
23
+ symbols = Hash.new{|h,k| h[k] = []}
24
+
25
+ # The compound initial state
26
+ init = []
27
+
28
+ # Build the symbol table and prepare the initial state
29
+ automata.each_with_index do |fa,i|
30
+ fa.alphabet.each{|l| symbols[l][i] = fa}
31
+ init << fa.initial_state
32
+ end
33
+
34
+ # Compound automaton and states already seen
35
+ compound_fa = Automaton.new
36
+ map = {init => compound_fa.add_state(marks(init, true))}
37
+
38
+ # States to be visited
39
+ to_visit = [init]
40
+
41
+ until to_visit.empty?
42
+ source = to_visit.pop
43
+ symbols.each_pair do |symbol, automata|
44
+ catch(:avoid) do
45
+ # build the target state
46
+ target = source.zip(automata).collect{|ss,a|
47
+ if a.nil?
48
+ # this automaton does no synchronize on symbol
49
+ ss
50
+ elsif tt = ss.dfa_delta(symbol)
51
+ # it synchronizes and target has been found
52
+ tt
53
+ else
54
+ # it synchronizes but target has not been found
55
+ throw :avoid
56
+ end
57
+ }
58
+ unless map.has_key?(target)
59
+ map[target] = compound_fa.add_state(marks(target))
60
+ to_visit << target
61
+ end
62
+ compound_fa.connect(map[source],map[target],symbol)
63
+ end
64
+ end
65
+ end # to_visit.empty?
66
+
67
+ compound_fa
68
+ end
69
+
70
+ def self.execute(automata)
71
+ Compose.new(automata).main
72
+ end
73
+
74
+ end # class Compose
75
+
76
+ def compose(*automata)
77
+ Automaton::Compose.execute([self] + automata)
78
+ end
79
+
80
+ end # class Automaton
81
+ end # module Stamina
82
+
83
+ # class CompoundState
84
+ # include Enumerable
85
+ # attr_reader :states
86
+ #
87
+ # def initialize(states)
88
+ # @states = states
89
+ # end
90
+ #
91
+ # def hash
92
+ # states.hash
93
+ # end
94
+ #
95
+ # def ==(other)
96
+ # other.states == states
97
+ # end
98
+ # alias :eql? :other
99
+ #
100
+ # def marks(initial = false)
101
+ # {:initial => initial,
102
+ # :accepting => states.all?{|s| s.accepting?},
103
+ # :error => states.any?{|s| s.error?}
104
+ # }
105
+ # end
106
+ #
107
+ # def each
108
+ # states.each &Proc.new
109
+ # end
110
+ #
111
+ # end
@@ -0,0 +1,104 @@
1
+ module Stamina
2
+ class Automaton
3
+ class Determinize
4
+
5
+ class CompoundState
6
+
7
+ attr_reader :fa
8
+ attr_reader :states
9
+ attr_reader :initial
10
+
11
+ def initialize(fa, states, initial = false)
12
+ @fa = fa
13
+ @states = states.sort
14
+ @initial = initial
15
+ end
16
+
17
+ def empty?
18
+ states.empty?
19
+ end
20
+
21
+ def marks
22
+ @marks ||= begin
23
+ marks = {}
24
+ marks[:initial] = initial
25
+ marks[:accepting] = states.any?{|s| s.accepting?}
26
+ marks[:error] = states.any?{|s| s.error?}
27
+ marks
28
+ end
29
+ end
30
+
31
+ def delta(symbol)
32
+ CompoundState.new(fa, fa.delta(states, symbol))
33
+ end
34
+
35
+ def hash
36
+ @states.hash
37
+ end
38
+
39
+ def ==(other)
40
+ other.is_a?(CompoundState) &&
41
+ (other.fa == self.fa) &&
42
+ (other.states == self.states)
43
+ end
44
+ alias :eql? :==
45
+
46
+ end # class CompoundState
47
+
48
+ attr_reader :fa
49
+ attr_reader :options
50
+
51
+ def initialize(fa, options)
52
+ @fa = fa
53
+ @options = options
54
+ end
55
+
56
+ def main
57
+ # the alphabet
58
+ alph = fa.alphabet
59
+
60
+ # the minimized automaton
61
+ minimized = Automaton.new
62
+
63
+ # - map between compound states and minimized states
64
+ # - states to visit
65
+ map = {}
66
+ to_visit = []
67
+
68
+ # initial state and mark as to visit
69
+ init = CompoundState.new(fa, fa.initial_states, true)
70
+ map[init] = minimized.add_state(init.marks)
71
+ to_visit = [init]
72
+
73
+ until to_visit.empty?
74
+ current = to_visit.pop
75
+ alph.each do |symbol|
76
+ found = current.delta(symbol)
77
+ next if found.empty?
78
+ unless map.has_key?(found)
79
+ map[found] = minimized.add_state(found.marks)
80
+ to_visit << found
81
+ end
82
+ minimized.connect(map[current], map[found], symbol)
83
+ end
84
+ end
85
+
86
+ minimized
87
+ end
88
+
89
+ def self.execute(fa, options = {})
90
+ Determinize.new(fa, options).main
91
+ end
92
+
93
+ end # class Determinize
94
+
95
+ #
96
+ # Determinizes this automaton by removing explicit non-determinism as well
97
+ # as all espilon moves.
98
+ #
99
+ def determinize
100
+ Determinize.execute(self)
101
+ end
102
+
103
+ end # class Automaton
104
+ end # module Stamina
@@ -0,0 +1,57 @@
1
+ module Stamina
2
+ class Automaton
3
+
4
+ #
5
+ # Checks if this automaton is equivalent to another one.
6
+ #
7
+ # Automata must be both minimal and complete to guarantee that this method
8
+ # works.
9
+ #
10
+ def equivalent?(other, equiv = nil, key = :equiv_state)
11
+ equiv ||= Proc.new{|s1,s2| (s1 && s2) &&
12
+ (s1.accepting? == s2.accepting?) &&
13
+ (s1.error? == s2.error?) &&
14
+ (s1.initial? == s2.initial?) }
15
+
16
+ # Both must already have basic attributes in common
17
+ return false unless state_count==other.state_count
18
+ return false unless edge_count==other.edge_count
19
+ return false unless alphabet==other.alphabet
20
+ return false unless equiv[initial_state, other.initial_state]
21
+
22
+ # We instantiate the decoration algorithm for checking equivalence on this
23
+ # automaton:
24
+ # * decoration is the index of the equivalent state in other automaton
25
+ # * d0 is thus 'other.initial_state.index'
26
+ # * suppremum is identity and fails when the equivalent state is not unique
27
+ # * propagation checks transition function delta
28
+ #
29
+ algo = Stamina::Utils::Decorate.new(key)
30
+ algo.set_suppremum do |d0, d1|
31
+ if (d0.nil? or d1.nil?)
32
+ (d0 || d1)
33
+ elsif d0==d1
34
+ d0
35
+ else
36
+ raise Stamina::Abord
37
+ end
38
+ end
39
+ algo.set_propagate do |d,e|
40
+ reached = other.ith_state(d).dfa_step(e.symbol)
41
+ raise Stamina::Abord if reached.nil?
42
+ raise Stamina::Abord unless equiv[e.target, reached]
43
+ reached.index
44
+ end
45
+
46
+ # Run the algorithm now
47
+ begin
48
+ algo.execute(self, nil, other.initial_state.index)
49
+ return true
50
+ rescue Stamina::Abord
51
+ return false
52
+ end
53
+ end
54
+ alias :<=> :equivalent?
55
+
56
+ end # class Automaton
57
+ end # module Stamina
@@ -0,0 +1,41 @@
1
+ module Stamina
2
+ class Automaton
3
+
4
+ #
5
+ # Returns a copy of self where all symbols in `alph` have been
6
+ # replaced by nil.
7
+ #
8
+ def hide(alph)
9
+ dup.hide!(alph)
10
+ end
11
+
12
+ #
13
+ # Replaces all symbols in `alph` by nil in this automaton.
14
+ #
15
+ def hide!(alph)
16
+ new_alph = alphabet.to_a - alph.to_a
17
+ h = Set.new(new_alph.to_a)
18
+ each_edge do |edge|
19
+ edge.symbol = nil unless h.include?(edge.symbol)
20
+ end
21
+ self.alphabet = new_alph
22
+ self
23
+ end
24
+
25
+ #
26
+ # Returns a copy of self where all symbols not in `alph` have been
27
+ # replaced by nil.
28
+ #
29
+ def keep(alph)
30
+ dup.keep!(alph)
31
+ end
32
+
33
+ #
34
+ # Replaces all symbols not in `alph` by nil in this automaton.
35
+ #
36
+ def keep!(alph)
37
+ hide!(self.alphabet.to_a - alph.to_a)
38
+ end
39
+
40
+ end # class Automaton
41
+ end # module Stamina
@@ -0,0 +1,77 @@
1
+ module Stamina
2
+ class Automaton
3
+ #
4
+ # Provides useful metric methods on automata.
5
+ #
6
+ # This module is automatically included by Automaton and is not intended
7
+ # to be used directly.
8
+ #
9
+ module Metrics
10
+
11
+ #
12
+ # Returns the number of letters of the alphabet.
13
+ #
14
+ def alphabet_size
15
+ alphabet.size
16
+ end
17
+
18
+ #
19
+ # Returns the average degree of states, that is,
20
+ # <code>edge_count/state_count</code>
21
+ #
22
+ def avg_degree
23
+ edge_count.to_f/state_count.to_f
24
+ end
25
+ alias :avg_out_degree :avg_degree
26
+ alias :avg_in_degree :avg_degree
27
+
28
+ #
29
+ # Number of accepting states over all states
30
+ #
31
+ def accepting_ratio
32
+ states.select{|s|s.accepting?}.size.to_f/state_count.to_f
33
+ end
34
+
35
+ #
36
+ # Number of error states over all states
37
+ #
38
+ def error_ratio
39
+ states.select{|s|s.error?}.size.to_f/state_count.to_f
40
+ end
41
+
42
+ #
43
+ # Computes the depth of the automaton.
44
+ #
45
+ # The depth of an automaton is defined as the length of the longest shortest
46
+ # path from the initial state to a state.
47
+ #
48
+ # This method has a side effect on state marks, as it keeps the depth of
49
+ # each state as a mark under _key_, which defaults to :depth.
50
+ #
51
+ def depth(key = :depth)
52
+ algo = Stamina::Utils::Decorate.new(key)
53
+ algo.set_suppremum do |d0,d1|
54
+ # Here, unreached state has the max value (i.e. nil is +INF)
55
+ # and we look at the minimum depth for each state
56
+ if d0.nil?
57
+ d1
58
+ elsif d1.nil?
59
+ d0
60
+ else
61
+ (d0 <= d1 ? d0 : d1)
62
+ end
63
+ end
64
+ algo.set_propagate {|d,e| d+1 }
65
+ algo.execute(self, nil, 0)
66
+ deepest = states.max do |s0,s1|
67
+ # Here, we do not take unreachable states into account
68
+ # so that -1 is taken when nil is encountered
69
+ (s0[:depth] || -1) <=> (s1[:depth] || -1)
70
+ end
71
+ deepest[:depth]
72
+ end
73
+
74
+ end # module Metrics
75
+ include Metrics
76
+ end # class Automaton
77
+ end # module Stamina