stamina 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +24 -0
- data/Gemfile.lock +5 -1
- data/bin/stamina +10 -0
- data/lib/stamina.rb +2 -1
- data/lib/stamina/abbadingo.rb +2 -0
- data/lib/stamina/abbadingo/random_dfa.rb +48 -0
- data/lib/stamina/abbadingo/random_sample.rb +146 -0
- data/lib/stamina/adl.rb +6 -6
- data/lib/stamina/automaton.rb +29 -4
- data/lib/stamina/automaton/complete.rb +36 -0
- data/lib/stamina/automaton/equivalence.rb +55 -0
- data/lib/stamina/automaton/metrics.rb +8 -1
- data/lib/stamina/automaton/minimize.rb +25 -0
- data/lib/stamina/automaton/minimize/hopcroft.rb +116 -0
- data/lib/stamina/automaton/minimize/pitchies.rb +64 -0
- data/lib/stamina/automaton/strip.rb +16 -0
- data/lib/stamina/automaton/walking.rb +46 -19
- data/lib/stamina/command.rb +45 -0
- data/lib/stamina/command/abbadingo_dfa.rb +81 -0
- data/lib/stamina/command/abbadingo_samples.rb +40 -0
- data/lib/stamina/command/adl2dot.rb +71 -0
- data/lib/stamina/command/classify.rb +48 -0
- data/lib/stamina/command/help.rb +27 -0
- data/lib/stamina/command/infer.rb +141 -0
- data/lib/stamina/command/metrics.rb +51 -0
- data/lib/stamina/command/robustness.rb +22 -0
- data/lib/stamina/command/score.rb +35 -0
- data/lib/stamina/errors.rb +4 -1
- data/lib/stamina/ext/math.rb +20 -0
- data/lib/stamina/induction/{redblue.rb → blue_fringe.rb} +29 -28
- data/lib/stamina/induction/commons.rb +32 -46
- data/lib/stamina/induction/rpni.rb +7 -9
- data/lib/stamina/induction/union_find.rb +3 -3
- data/lib/stamina/loader.rb +1 -0
- data/lib/stamina/sample.rb +79 -2
- data/lib/stamina/scoring.rb +37 -0
- data/lib/stamina/version.rb +2 -2
- data/stamina.gemspec +2 -1
- data/stamina.noespec +9 -12
- data/test/stamina/abbadingo/random_dfa_test.rb +16 -0
- data/test/stamina/abbadingo/random_sample_test.rb +78 -0
- data/test/stamina/adl_test.rb +27 -2
- data/test/stamina/automaton/complete_test.rb +58 -0
- data/test/stamina/automaton/equivalence_test.rb +120 -0
- data/test/stamina/automaton/minimize/hopcroft_test.rb +15 -0
- data/test/stamina/automaton/minimize/minimize_test.rb +55 -0
- data/test/stamina/automaton/minimize/pitchies_test.rb +15 -0
- data/test/stamina/automaton/minimize/rice_edu_10.adl +16 -0
- data/test/stamina/automaton/minimize/rice_edu_10.min.adl +13 -0
- data/test/stamina/automaton/minimize/rice_edu_13.adl +13 -0
- data/test/stamina/automaton/minimize/rice_edu_13.min.adl +7 -0
- data/test/stamina/automaton/minimize/should_strip_1.adl +8 -0
- data/test/stamina/automaton/minimize/should_strip_1.min.adl +6 -0
- data/test/stamina/automaton/minimize/unknown_1.adl +16 -0
- data/test/stamina/automaton/minimize/unknown_1.min.adl +12 -0
- data/test/stamina/automaton/strip_test.rb +36 -0
- data/test/stamina/automaton/walking/dfa_delta_test.rb +39 -0
- data/test/stamina/automaton_test.rb +13 -1
- data/test/stamina/induction/{redblue_test.rb → blue_fringe_test.rb} +22 -22
- data/test/stamina/sample_test.rb +75 -0
- data/test/stamina/stamina_test.rb +13 -2
- metadata +98 -23
- data/bin/adl2dot +0 -12
- data/bin/classify +0 -12
- data/bin/redblue +0 -12
- data/bin/rpni +0 -12
- data/lib/stamina/command/adl2dot_command.rb +0 -73
- data/lib/stamina/command/classify_command.rb +0 -57
- data/lib/stamina/command/redblue_command.rb +0 -58
- data/lib/stamina/command/rpni_command.rb +0 -58
- data/lib/stamina/command/stamina_command.rb +0 -79
data/lib/stamina/errors.rb
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
module Stamina
|
2
2
|
|
3
|
+
# Raised when an algorithm explicitely abords something
|
4
|
+
class Abord < StandardError; end
|
5
|
+
|
3
6
|
# Main class of all stamina errors.
|
4
7
|
class StaminaError < StandardError; end
|
5
8
|
|
@@ -17,4 +20,4 @@ module Stamina
|
|
17
20
|
|
18
21
|
end
|
19
22
|
|
20
|
-
end # module Stamina
|
23
|
+
end # module Stamina
|
@@ -0,0 +1,20 @@
|
|
1
|
+
if RUBY_VERSION < "1.9"
|
2
|
+
|
3
|
+
def Math.log2( x )
|
4
|
+
Math.log( x ) / Math.log( 2 )
|
5
|
+
end
|
6
|
+
|
7
|
+
def Math.logn( x, n )
|
8
|
+
Math.log( x ) / Math.log( n )
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
def Math.max(i, j)
|
14
|
+
i > j ? i : j
|
15
|
+
end
|
16
|
+
|
17
|
+
def Math.min(i, j)
|
18
|
+
i < j ? i : j
|
19
|
+
end
|
20
|
+
|
@@ -2,7 +2,7 @@ module Stamina
|
|
2
2
|
module Induction
|
3
3
|
|
4
4
|
#
|
5
|
-
# Implementation of the
|
5
|
+
# Implementation of the BlueFringe variant of the RPNI algorithm (with the blue-fringe
|
6
6
|
# heuristics).
|
7
7
|
#
|
8
8
|
# See Lang, K., B. Pearlmutter, andR. Price. 1998. Results of the Abbadingo One DFA
|
@@ -13,34 +13,31 @@ module Stamina
|
|
13
13
|
# # sample typically comes from an ADL file
|
14
14
|
# sample = Stamina::ADL.parse_sample_file('sample.adl')
|
15
15
|
#
|
16
|
-
# # let
|
17
|
-
# dfa = Stamina::Induction::
|
16
|
+
# # let BlueFringe build the smallest dfa
|
17
|
+
# dfa = Stamina::Induction::BlueFringe.execute(sample, {:verbose => true})
|
18
18
|
#
|
19
19
|
# Remarks:
|
20
20
|
# - Constructor and instance methods of this class are public but not intended
|
21
21
|
# to be used directly. They are left public for testing purposes only.
|
22
|
-
# - Having read the Stamina::Induction::
|
22
|
+
# - Having read the Stamina::Induction::BlueFringe base algorithm may help undertanding
|
23
23
|
# this variant.
|
24
24
|
# - This class intensively uses the Stamina::Induction::UnionFind class and
|
25
25
|
# methods defined in the Stamina::Induction::Commons module which are worth
|
26
26
|
# reading to understand the algorithm implementation.
|
27
27
|
#
|
28
|
-
class
|
28
|
+
class BlueFringe
|
29
29
|
include Stamina::Induction::Commons
|
30
30
|
|
31
31
|
# Union-find data structure used internally
|
32
32
|
attr_reader :ufds
|
33
33
|
|
34
|
-
#
|
35
|
-
attr_reader :options
|
36
|
-
|
37
|
-
#
|
38
|
-
# Creates an algorithm instance with specific options
|
39
|
-
#
|
34
|
+
# Creates an algorithm instance with given options.
|
40
35
|
def initialize(options={})
|
41
|
-
|
36
|
+
raise ArgumentError, "Invalid options #{options.inspect}" unless options.is_a?(Hash)
|
37
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
38
|
+
@score_cache = {}
|
42
39
|
end
|
43
|
-
|
40
|
+
|
44
41
|
#
|
45
42
|
# Computes the score of a single (group) merge. Returned value is 1 if both are
|
46
43
|
# accepting states or both are error states and 0 otherwise. Note that d1 and d2
|
@@ -123,13 +120,16 @@ module Stamina
|
|
123
120
|
# been evaluated and is then seen unchanged by the caller.
|
124
121
|
#
|
125
122
|
def merge_and_determinize_score(i, j)
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
123
|
+
score = @score_cache[[i,j]] ||= begin
|
124
|
+
# score the merging, always rollback the transaction
|
125
|
+
score = nil
|
126
|
+
@ufds.transactional do
|
127
|
+
score = merge_and_determinize(i, j)
|
128
|
+
false
|
129
|
+
end
|
130
|
+
score || -1
|
131
131
|
end
|
132
|
-
score
|
132
|
+
score == -1 ? nil : score
|
133
133
|
end
|
134
134
|
|
135
135
|
#
|
@@ -163,8 +163,8 @@ module Stamina
|
|
163
163
|
# sample are correctly classified by it.
|
164
164
|
#
|
165
165
|
def main(ufds)
|
166
|
-
|
167
|
-
@ufds, @kernel = ufds, [0]
|
166
|
+
info("Starting BlueFringe (#{ufds.size} states)")
|
167
|
+
@ufds, @kernel, @score_cache = ufds, [0], {}
|
168
168
|
|
169
169
|
# we do it until the fringe is empty (compute it only once each step)
|
170
170
|
until (the_fringe=fringe).empty?
|
@@ -196,15 +196,16 @@ module Stamina
|
|
196
196
|
# If not found, the last candidate must be consolidated. Otherwise, we
|
197
197
|
# do the best merging
|
198
198
|
unless to_consolidate.nil?
|
199
|
-
|
199
|
+
info("Consolidation of #{to_consolidate}")
|
200
200
|
@kernel << to_consolidate
|
201
201
|
else
|
202
|
-
|
202
|
+
@score_cache.clear
|
203
|
+
info("Merging #{best[0]} and #{best[1]} [#{best[2]}]")
|
203
204
|
# this one should never fail because its score was positive before
|
204
205
|
raise "Unexpected case" unless merge_and_determinize(best[0], best[1])
|
205
206
|
end
|
206
207
|
|
207
|
-
#
|
208
|
+
# blue_fringe does not guarantee that it will not merge a state of lower rank
|
208
209
|
# with a kernel state. The kernel should then be update at each step to keep
|
209
210
|
# lowest indices for the whole kernel, and we sort it
|
210
211
|
@kernel = @kernel.collect{|k| @ufds.find(k)}.sort
|
@@ -226,13 +227,13 @@ module Stamina
|
|
226
227
|
# given as input.
|
227
228
|
#
|
228
229
|
# Remarks:
|
229
|
-
# - This instance version of
|
230
|
+
# - This instance version of BlueFringe.execute is not intended to be used directly and
|
230
231
|
# is mainly provided for testing purposes. Please use the class variant of this
|
231
232
|
# method if possible.
|
232
233
|
#
|
233
234
|
def execute(sample)
|
234
235
|
# create union-find
|
235
|
-
|
236
|
+
info("Creating PTA and UnionFind structure")
|
236
237
|
ufds = sample2ufds(sample)
|
237
238
|
# refine it
|
238
239
|
ufds = main(ufds)
|
@@ -255,10 +256,10 @@ module Stamina
|
|
255
256
|
# given as input.
|
256
257
|
#
|
257
258
|
def self.execute(sample, options={})
|
258
|
-
|
259
|
+
BlueFringe.new(options).execute(sample)
|
259
260
|
end
|
260
261
|
|
261
|
-
end # class
|
262
|
+
end # class BlueFringe
|
262
263
|
|
263
264
|
end # module Induction
|
264
265
|
end # module Stamina
|
@@ -2,20 +2,45 @@ module Stamina
|
|
2
2
|
module Induction
|
3
3
|
|
4
4
|
#
|
5
|
-
# Defines common utilities used by rpni and
|
5
|
+
# Defines common utilities used by rpni and blue_fringe. About acronyms:
|
6
6
|
# - _pta_ stands for Prefix Tree Acceptor
|
7
7
|
# - _ufds_ stands for Union-Find Data Structure
|
8
8
|
#
|
9
|
-
# Methods pta2ufds
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
9
|
+
# Methods pta2ufds and sample2ufds are simply conversion methods used when the induction
|
10
|
+
# algorithm starts (executed on a sample, it first built a pta then convert it to a union
|
11
|
+
# find). Method ufds2dfa is used when the algorithm ends, to convert refined union find to
|
12
|
+
# a dfa.
|
13
13
|
#
|
14
14
|
# The merge_user_data method is probably the most important as it actually computes
|
15
15
|
# the merging of two states and build information about merging for determinization.
|
16
16
|
#
|
17
17
|
module Commons
|
18
18
|
|
19
|
+
DEFAULT_OPTIONS = {
|
20
|
+
:verbose => false,
|
21
|
+
:verbose_io => $stderr
|
22
|
+
}
|
23
|
+
|
24
|
+
# Additional options of the algorithm
|
25
|
+
attr_reader :options
|
26
|
+
|
27
|
+
# Is the verbose mode on ?
|
28
|
+
def verbose?
|
29
|
+
@verbose ||= !!options[:verbose]
|
30
|
+
end
|
31
|
+
|
32
|
+
def verbose_io
|
33
|
+
@verbose_io ||= options[:verbose_io] || $stderr
|
34
|
+
end
|
35
|
+
|
36
|
+
# Display an information message (when verbose)
|
37
|
+
def info(msg)
|
38
|
+
if verbose?
|
39
|
+
verbose_io << msg << "\n"
|
40
|
+
verbose_io.flush
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
19
44
|
#
|
20
45
|
# Factors and returns a UnionFind data structure from a PTA, keeping natural order
|
21
46
|
# of its states for union-find elements. The resulting UnionFind contains a Hash as
|
@@ -47,46 +72,7 @@ module Stamina
|
|
47
72
|
# non accepting and error.
|
48
73
|
#
|
49
74
|
def sample2pta(sample)
|
50
|
-
|
51
|
-
initial_state = add_state(:initial => true, :accepting => false)
|
52
|
-
|
53
|
-
# Fill the PTA with each string
|
54
|
-
sample.each do |str|
|
55
|
-
# split string using the dfa
|
56
|
-
parsed, reached, remaining = pta.dfa_split(str, initial_state)
|
57
|
-
|
58
|
-
# remaining symbols are not empty -> build the PTA
|
59
|
-
unless remaining.empty?
|
60
|
-
remaining.each do |symbol|
|
61
|
-
newone = pta.add_state(:initial => false, :accepting => false, :error => false)
|
62
|
-
pta.connect(reached, newone, symbol)
|
63
|
-
reached = newone
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# flag state
|
68
|
-
str.positive? ? reached.accepting! : reached.error!
|
69
|
-
|
70
|
-
# check consistency, should not arrive as Sample does not allow
|
71
|
-
# inconsistencies. Should appear only if _sample_ is not a Sample
|
72
|
-
# instance but some other enumerable.
|
73
|
-
raise(InconsistencyError, "Inconsistent sample on #{str}", caller)\
|
74
|
-
if (reached.error? and reached.accepting?)
|
75
|
-
end
|
76
|
-
|
77
|
-
# Reindex states by applying BFS
|
78
|
-
to_index, index = [initial_state], 0
|
79
|
-
until to_index.empty?
|
80
|
-
state = to_index.shift
|
81
|
-
state[:__index__] = index
|
82
|
-
state.out_edges.sort{|e,f| e.symbol<=>f.symbol}.each {|e| to_index << e.target}
|
83
|
-
index += 1
|
84
|
-
end
|
85
|
-
# Force the automaton to reindex
|
86
|
-
pta.order_states{|s0,s1| s0[:__index__]<=>s1[:__index__]}
|
87
|
-
# Remove marks
|
88
|
-
pta.states.each{|s| s.remove_mark(:__index__)}
|
89
|
-
end
|
75
|
+
sample.to_pta
|
90
76
|
end
|
91
77
|
|
92
78
|
#
|
@@ -167,4 +153,4 @@ module Stamina
|
|
167
153
|
end # module Commons
|
168
154
|
|
169
155
|
end # module Induction
|
170
|
-
end # module Stamina
|
156
|
+
end # module Stamina
|
@@ -31,14 +31,12 @@ module Stamina
|
|
31
31
|
# Union-find data structure used internally
|
32
32
|
attr_reader :ufds
|
33
33
|
|
34
|
-
# Additional options of the algorithm
|
35
|
-
attr_reader :options
|
36
|
-
|
37
34
|
# Creates an algorithm instance with given options.
|
38
35
|
def initialize(options={})
|
39
|
-
|
36
|
+
raise ArgumentError, "Invalid options #{options.inspect}" unless options.is_a?(Hash)
|
37
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
40
38
|
end
|
41
|
-
|
39
|
+
|
42
40
|
#
|
43
41
|
# Merges a state of rank j with a state of lower rank i. This merge method
|
44
42
|
# includes merging for determinization.
|
@@ -118,7 +116,7 @@ module Stamina
|
|
118
116
|
#
|
119
117
|
def main(ufds)
|
120
118
|
@ufds = ufds
|
121
|
-
|
119
|
+
info("Starting RPNI (#{@ufds.size} states)")
|
122
120
|
# First loop, iterating all PTA states
|
123
121
|
(1...@ufds.size).each do |i|
|
124
122
|
# we ignore those that have been previously merged
|
@@ -130,7 +128,7 @@ module Stamina
|
|
130
128
|
# simply break the loop if it works!
|
131
129
|
success = successfull_merge_or_nothing(i,j)
|
132
130
|
if success
|
133
|
-
|
131
|
+
info("#{i} and #{j} successfully merged")
|
134
132
|
break
|
135
133
|
end
|
136
134
|
end # j loop
|
@@ -156,7 +154,7 @@ module Stamina
|
|
156
154
|
#
|
157
155
|
def execute(sample)
|
158
156
|
# create union-find
|
159
|
-
|
157
|
+
info("Creating PTA and UnionFind structure")
|
160
158
|
ufds = sample2ufds(sample)
|
161
159
|
# refine it
|
162
160
|
ufds = main(ufds)
|
@@ -185,4 +183,4 @@ module Stamina
|
|
185
183
|
end # class RPNI
|
186
184
|
|
187
185
|
end # module Induction
|
188
|
-
end # module Stamina
|
186
|
+
end # module Stamina
|
@@ -86,7 +86,7 @@ module Stamina
|
|
86
86
|
# == Transactional support
|
87
87
|
#
|
88
88
|
# The main aim of this UnionFind is to make the implementation induction algorithms
|
89
|
-
# Stamina::Induction::RPNI and Stamina::Induction::
|
89
|
+
# Stamina::Induction::RPNI and Stamina::Induction::BlueFringe (sufficiently) efficient,
|
90
90
|
# simple and readable. These algorithms rely on a try-and-error strategy are must be
|
91
91
|
# able to revert the changes they have made during their last try. The transaction
|
92
92
|
# support implemented by this data structure helps them achieving this goal. For this
|
@@ -129,7 +129,7 @@ module Stamina
|
|
129
129
|
# Duplicates this node, ensuring that future changes will not affect the copy.
|
130
130
|
# Please note that the user data itself is not duplicated and is not expected
|
131
131
|
# to change. This property (not changing user data) is respected by the RPNI
|
132
|
-
# and
|
132
|
+
# and BlueFringe classes as implemented in this library.
|
133
133
|
#
|
134
134
|
def dup
|
135
135
|
Node.new(@parent, @data)
|
@@ -374,4 +374,4 @@ module Stamina
|
|
374
374
|
end # class UnionFind
|
375
375
|
|
376
376
|
end # module Induction
|
377
|
-
end # module Stamina
|
377
|
+
end # module Stamina
|
data/lib/stamina/loader.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
require "quickl"
|
data/lib/stamina/sample.rb
CHANGED
@@ -28,9 +28,10 @@ module Stamina
|
|
28
28
|
#
|
29
29
|
# Creates an empty sample.
|
30
30
|
#
|
31
|
-
def initialize()
|
31
|
+
def initialize(strings = nil)
|
32
32
|
@strings = []
|
33
33
|
@size, @positive_count, @negative_count = 0, 0, 0
|
34
|
+
strings.each{|s| self << s } unless strings.nil?
|
34
35
|
end
|
35
36
|
|
36
37
|
#
|
@@ -175,6 +176,16 @@ module Stamina
|
|
175
176
|
end
|
176
177
|
signature
|
177
178
|
end
|
179
|
+
|
180
|
+
#
|
181
|
+
# Takes only a given proportion of this sample and returns it as a new Sample.
|
182
|
+
#
|
183
|
+
def take(proportion = 0.5)
|
184
|
+
taken = Stamina::Sample.new
|
185
|
+
each_positive{|s| taken << s if Kernel.rand < proportion}
|
186
|
+
each_negative{|s| taken << s if Kernel.rand < proportion}
|
187
|
+
taken
|
188
|
+
end
|
178
189
|
|
179
190
|
#
|
180
191
|
# Prints an ADL description of this sample on the buffer.
|
@@ -184,7 +195,73 @@ module Stamina
|
|
184
195
|
end
|
185
196
|
alias :to_s :to_adl
|
186
197
|
alias :inspect :to_adl
|
198
|
+
|
199
|
+
#
|
200
|
+
# Converts a Sample to an (augmented) prefix tree acceptor. This method ensures
|
201
|
+
# that the states of the PTA are in lexical order, according to the <code><=></code>
|
202
|
+
# operator defined on symbols. States reached by negative strings are tagged as
|
203
|
+
# non accepting and error.
|
204
|
+
#
|
205
|
+
def self.to_pta(sample)
|
206
|
+
thepta = Automaton.new do |pta|
|
207
|
+
initial_state = add_state(:initial => true, :accepting => false)
|
208
|
+
|
209
|
+
# Fill the PTA with each string
|
210
|
+
sample.each do |str|
|
211
|
+
# split string using the dfa
|
212
|
+
parsed, reached, remaining = pta.dfa_split(str, initial_state)
|
187
213
|
|
188
|
-
|
214
|
+
# remaining symbols are not empty -> build the PTA
|
215
|
+
unless remaining.empty?
|
216
|
+
remaining.each do |symbol|
|
217
|
+
newone = pta.add_state(:initial => false, :accepting => false, :error => false)
|
218
|
+
pta.connect(reached, newone, symbol)
|
219
|
+
reached = newone
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# flag state
|
224
|
+
str.positive? ? reached.accepting! : reached.error!
|
225
|
+
|
226
|
+
# check consistency, should not arrive as Sample does not allow
|
227
|
+
# inconsistencies. Should appear only if _sample_ is not a Sample
|
228
|
+
# instance but some other enumerable.
|
229
|
+
raise(InconsistencyError, "Inconsistent sample on #{str}", caller)\
|
230
|
+
if (reached.error? and reached.accepting?)
|
231
|
+
end
|
189
232
|
|
233
|
+
# Reindex states by applying BFS
|
234
|
+
to_index, index = [initial_state], 0
|
235
|
+
until to_index.empty?
|
236
|
+
state = to_index.shift
|
237
|
+
state[:__index__] = index
|
238
|
+
state.out_edges.sort{|e,f| e.symbol<=>f.symbol}.each{|e| to_index << e.target}
|
239
|
+
index += 1
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
# Now we rebuild a fresh one with states in order.
|
244
|
+
# This look more efficient that reordering states of the PTA
|
245
|
+
Automaton.new do |ordered|
|
246
|
+
ordered.add_n_states(thepta.state_count)
|
247
|
+
thepta.each_state do |pta_state|
|
248
|
+
source = ordered.ith_state(pta_state[:__index__])
|
249
|
+
source.initial! if pta_state.initial?
|
250
|
+
source.accepting! if pta_state.accepting?
|
251
|
+
source.error! if pta_state.error?
|
252
|
+
pta_state.out_edges.each do |e|
|
253
|
+
target = ordered.ith_state(e.target[:__index__])
|
254
|
+
ordered.connect(source, target, e.symbol)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
end
|
260
|
+
|
261
|
+
# Convenient shortcut for Sample.to_pta(sample_instance)
|
262
|
+
def to_pta
|
263
|
+
Sample.to_pta(self)
|
264
|
+
end
|
265
|
+
|
266
|
+
end # class Sample
|
190
267
|
end # module Stamina
|