stamina-core 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +78 -0
- data/LICENCE.md +22 -0
- data/lib/stamina-core/stamina-core.rb +1 -0
- data/lib/stamina-core/stamina/adl.rb +298 -0
- data/lib/stamina-core/stamina/automaton.rb +1300 -0
- data/lib/stamina-core/stamina/automaton/complement.rb +26 -0
- data/lib/stamina-core/stamina/automaton/complete.rb +36 -0
- data/lib/stamina-core/stamina/automaton/compose.rb +111 -0
- data/lib/stamina-core/stamina/automaton/determinize.rb +104 -0
- data/lib/stamina-core/stamina/automaton/equivalence.rb +57 -0
- data/lib/stamina-core/stamina/automaton/hide.rb +41 -0
- data/lib/stamina-core/stamina/automaton/metrics.rb +77 -0
- data/lib/stamina-core/stamina/automaton/minimize.rb +23 -0
- data/lib/stamina-core/stamina/automaton/minimize/hopcroft.rb +118 -0
- data/lib/stamina-core/stamina/automaton/minimize/pitchies.rb +130 -0
- data/lib/stamina-core/stamina/automaton/strip.rb +16 -0
- data/lib/stamina-core/stamina/automaton/walking.rb +361 -0
- data/lib/stamina-core/stamina/command.rb +38 -0
- data/lib/stamina-core/stamina/command/adl2dot.rb +82 -0
- data/lib/stamina-core/stamina/command/help.rb +23 -0
- data/lib/stamina-core/stamina/command/robustness.rb +21 -0
- data/lib/stamina-core/stamina/command/run.rb +84 -0
- data/lib/stamina-core/stamina/core.rb +11 -0
- data/lib/stamina-core/stamina/dsl.rb +6 -0
- data/lib/stamina-core/stamina/dsl/automata.rb +23 -0
- data/lib/stamina-core/stamina/dsl/core.rb +14 -0
- data/lib/stamina-core/stamina/engine.rb +32 -0
- data/lib/stamina-core/stamina/engine/context.rb +35 -0
- data/lib/stamina-core/stamina/errors.rb +26 -0
- data/lib/stamina-core/stamina/ext/math.rb +19 -0
- data/lib/stamina-core/stamina/loader.rb +3 -0
- data/lib/stamina-core/stamina/markable.rb +42 -0
- data/lib/stamina-core/stamina/utils.rb +1 -0
- data/lib/stamina-core/stamina/utils/decorate.rb +81 -0
- data/lib/stamina-core/stamina/version.rb +14 -0
- 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
|