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