stamina 0.3.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 (80) hide show
  1. data/.gemtest +0 -0
  2. data/CHANGELOG.md +22 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +33 -0
  5. data/LICENCE.md +22 -0
  6. data/Manifest.txt +16 -0
  7. data/README.md +78 -0
  8. data/Rakefile +23 -0
  9. data/bin/adl2dot +12 -0
  10. data/bin/classify +12 -0
  11. data/bin/redblue +12 -0
  12. data/bin/rpni +12 -0
  13. data/example/adl/automaton.adl +49 -0
  14. data/example/adl/sample.adl +53 -0
  15. data/example/basic/characteristic_sample.adl +32 -0
  16. data/example/basic/target.adl +9 -0
  17. data/example/competition/31_test.adl +1500 -0
  18. data/example/competition/31_training.adl +1759 -0
  19. data/lib/stamina.rb +19 -0
  20. data/lib/stamina/adl.rb +298 -0
  21. data/lib/stamina/automaton.rb +1237 -0
  22. data/lib/stamina/automaton/walking.rb +336 -0
  23. data/lib/stamina/classifier.rb +37 -0
  24. data/lib/stamina/command/adl2dot_command.rb +73 -0
  25. data/lib/stamina/command/classify_command.rb +57 -0
  26. data/lib/stamina/command/redblue_command.rb +58 -0
  27. data/lib/stamina/command/rpni_command.rb +58 -0
  28. data/lib/stamina/command/stamina_command.rb +79 -0
  29. data/lib/stamina/errors.rb +20 -0
  30. data/lib/stamina/induction/commons.rb +170 -0
  31. data/lib/stamina/induction/redblue.rb +264 -0
  32. data/lib/stamina/induction/rpni.rb +188 -0
  33. data/lib/stamina/induction/union_find.rb +377 -0
  34. data/lib/stamina/input_string.rb +123 -0
  35. data/lib/stamina/loader.rb +0 -0
  36. data/lib/stamina/markable.rb +42 -0
  37. data/lib/stamina/sample.rb +190 -0
  38. data/lib/stamina/version.rb +14 -0
  39. data/stamina.gemspec +190 -0
  40. data/stamina.noespec +35 -0
  41. data/tasks/debug_mail.rake +78 -0
  42. data/tasks/debug_mail.txt +13 -0
  43. data/tasks/gem.rake +68 -0
  44. data/tasks/spec_test.rake +79 -0
  45. data/tasks/unit_test.rake +77 -0
  46. data/tasks/yard.rake +51 -0
  47. data/test/stamina/adl_test.rb +491 -0
  48. data/test/stamina/automaton_additional_test.rb +190 -0
  49. data/test/stamina/automaton_classifier_test.rb +155 -0
  50. data/test/stamina/automaton_test.rb +1092 -0
  51. data/test/stamina/automaton_to_dot_test.rb +64 -0
  52. data/test/stamina/automaton_walking_test.rb +206 -0
  53. data/test/stamina/exit.rb +3 -0
  54. data/test/stamina/induction/induction_test.rb +70 -0
  55. data/test/stamina/induction/redblue_mergesamestatebug_expected.adl +19 -0
  56. data/test/stamina/induction/redblue_mergesamestatebug_pta.dot +64 -0
  57. data/test/stamina/induction/redblue_mergesamestatebug_sample.adl +9 -0
  58. data/test/stamina/induction/redblue_test.rb +83 -0
  59. data/test/stamina/induction/redblue_universal_expected.adl +4 -0
  60. data/test/stamina/induction/redblue_universal_sample.adl +5 -0
  61. data/test/stamina/induction/rpni_inria_expected.adl +7 -0
  62. data/test/stamina/induction/rpni_inria_sample.adl +9 -0
  63. data/test/stamina/induction/rpni_test.rb +129 -0
  64. data/test/stamina/induction/rpni_test_pta.dot +22 -0
  65. data/test/stamina/induction/rpni_universal_expected.adl +4 -0
  66. data/test/stamina/induction/rpni_universal_sample.adl +4 -0
  67. data/test/stamina/induction/union_find_test.rb +124 -0
  68. data/test/stamina/input_string_test.rb +323 -0
  69. data/test/stamina/markable_test.rb +70 -0
  70. data/test/stamina/randdfa.adl +66 -0
  71. data/test/stamina/sample.adl +4 -0
  72. data/test/stamina/sample_classify_test.rb +149 -0
  73. data/test/stamina/sample_test.rb +218 -0
  74. data/test/stamina/small_dfa.dot +16 -0
  75. data/test/stamina/small_dfa.gif +0 -0
  76. data/test/stamina/small_nfa.dot +18 -0
  77. data/test/stamina/small_nfa.gif +0 -0
  78. data/test/stamina/stamina_test.rb +69 -0
  79. data/test/test_all.rb +7 -0
  80. metadata +279 -0
@@ -0,0 +1,336 @@
1
+ module Stamina
2
+ class Automaton
3
+ #
4
+ # Provides useful automaton walking methods. This module is automatically
5
+ # included in Automaton and is not intended to be used directly.
6
+ #
7
+ # == Examples
8
+ # # Building an automaton for the regular language a(ba)*
9
+ # s0, s1 = nil
10
+ # fa = Automaton.new do
11
+ # s0 = add_state(:initial => true)
12
+ # s1 = add_state(:accepting => true)
13
+ # connect(0,1,'a')
14
+ # connect(1,0,'b')
15
+ # end
16
+ #
17
+ # # some examples with reached
18
+ # fa.dfa_reached('? a b') # -> s0 dfa variant method
19
+ # fa.dfa_reached('? a a') # -> nil
20
+ # fa.dfa_reached('? b a', s1) # -> s1 from an explicit init state
21
+ #
22
+ # fa.reached('? a b') # -> [s0] generic method on automaton
23
+ # fa.reached('? a a') # -> []
24
+ # fa.reached('? b a', s1) # -> [s1]
25
+ #
26
+ # # some examples with split (the most powerful one!)
27
+ # fa.dfa_split('? a b a b') # [['a','b','a','b'], s0, []]
28
+ # fa.dfa_split('? a b b a') # [['a','b'], s0, ['b','a']]
29
+ #
30
+ # fa.split('? a b a b') # [['a','b','a','b'], [s0], []]
31
+ # fa.split('? a b b a') # [['a','b'], [s0], ['b','a']]
32
+ #
33
+ # # All of this works on non-deterministic automata as well (and epsilon
34
+ # # symbols are taken into account), but you'll probably need to read
35
+ # # the following section to master the power of this module in this case!
36
+ #
37
+ # == Using this module
38
+ # This section fully details the design choices that has been made for the
39
+ # implementation of the Walking module used by Stamina on Automaton. It is provided
40
+ # because Walking is one of the core classes of Stamina, that probably all
41
+ # users (and contributors) will use. Walking usage is really user-friendly,
42
+ # so <b>you are normally not required</b> to read this section in the first
43
+ # place ! Read it only if of interest for you, or if you experiment unexpected
44
+ # results.
45
+ #
46
+ # Methods defined by this module respect common conventions that you must be
47
+ # aware of:
48
+ #
49
+ # === Generic methods vs. dfa variants
50
+ # The convention is simple: methods whose name starts with 'dfa_' are expected
51
+ # to be used on deterministic automata only (that is, automata answering _true_
52
+ # to the deterministic? method invocation). We refer to those methods as
53
+ # 'dfa variants'. Strange results may occur if invoked on non-deterministic
54
+ # automata. Other methods are called 'generic methods' and can be used on any
55
+ # automaton. Generic methods and dfa variants sometimes use different conventions
56
+ # according to arguments and returned values, as explained below.
57
+ #
58
+ # === Argument conventions
59
+ # - all methods taking a _symbol_ argument expect it to be a valid instance of
60
+ # the class used for representing input symbols on edges of your automaton
61
+ # (that is, the mark you've installed under :symbol key on the edge, see
62
+ # Automaton documentation for details).
63
+ # - all methods taking an _input_ argument support the following objects for it:
64
+ # - InputString instance: an real input string typically coming from a Sample.
65
+ # - Array of symbols: where by symbol, we mean an input symbol as explained
66
+ # above (and not a Ruby Symbol instance). The array is never modified by the
67
+ # methods, so that you don't have to worry about where this array comes from.
68
+ # - String (a real Ruby one): in this case, the input is expected to be an ADL
69
+ # input string, which is parsed using ADL::parse_string. Note that 'a b a b'
70
+ # is NOT a valid ADL input string, so that you typically have to use the '?'
71
+ # sign to indicate that the tested string is basically unlabeled.
72
+ # - all methods taking a _from_ argument support the following objects for it:
73
+ # - ommited: _from_ is interpreted as the set of initial states by generic
74
+ # methods and the last rule applies. _from_ is interpreted as the unique initial
75
+ # state of the deterministic automaton by dfa method variants (<tt>dfa_xxx</tt>),
76
+ # and the third rule applies.
77
+ # - Integer: _from_ is interpreted as a state index, and the next rule applies
78
+ # on the index-th state of the automaton.
79
+ # - State: _from_ is interpreted by the generic methods as a singleton set
80
+ # containing the state and the last rule applies. Deterministic method
81
+ # variants interpret it as the start state from which the walk must start.
82
+ # In this case, they always return a State instance (or _nil_) instead of
83
+ # an array of states.
84
+ # - Array: _from_ is interpreted as a set of states (duplicates are supported
85
+ # so it's actually a bag) from which the walk must start. Indexes of states
86
+ # are also supported, see Automaton documentation about indexes.
87
+ #
88
+ # === Returned value conventions
89
+ # Moreover, (unless stated explicitely) all methods returning states as (part of)
90
+ # their returned value respect the following _return_ conventions (which somewhat
91
+ # summarizes the _from_ conventions above):
92
+ # - generic methods *always* return an array of states (without duplicates) which
93
+ # can be modified. This array is *never* sorted by state index. To insist:
94
+ # even when invoked on a deterministic automaton with a State argument as
95
+ # _from_, they will return an array of states as show by the code excerpt
96
+ # below. Lastly, the returned value is *never* _nil_, but an empty array may
97
+ # be returned when it makes sense (no reached states for example).
98
+ #
99
+ # fa = Automaton.new do ... end # build a(ba)* automaton
100
+ # s0 = fa.initial_state
101
+ # fa.reached('? a b a b', s0) # returns [s0], not s0 !
102
+ #
103
+ # - dfa variant methods respond to your query using the same language as you:
104
+ # if _from_ is ommitted, is a State or an Integer, the the result will be a
105
+ # single State instance, or _nil_ if it makes sense (no reached state for
106
+ # example). Otherwise, they behaves exactly as generic methods (*always* return
107
+ # an array of states, ...)
108
+ #
109
+ # === Epsilon symbol aware methods
110
+ # Stamina does not allow epsilon symbols on deterministic automata; thus, this
111
+ # subsection only applies to generic methods.
112
+ #
113
+ # Methods documented as 'epsilon aware' (almost all generic methods) *always*
114
+ # take epsilon symbols into account in their computations (Stamina uses _nil_ as
115
+ # epsilon symbol, by convention), in a natural way. For example:
116
+ #
117
+ # fa = Automaton.new do ... end # build a non-deterministic automaton
118
+ # # with epsilon symbols
119
+ #
120
+ # # the line below computes the set of reached states
121
+ # # (from the set of initial states) by walking the dfa
122
+ # # with a string.
123
+ # #
124
+ # # The actual computation is in fact the set of reached
125
+ # # states with the string (regex) 'eps* a eps* b eps*',
126
+ # # where eps is the epsilon symbol.
127
+ # reached = fa.reached('? a b')
128
+ #
129
+ # == Detailed API
130
+ module Walking
131
+
132
+ #
133
+ # Returns reachable states from _from_ states with an input _symbol_. This
134
+ # method is not epsilon symbol aware.
135
+ #
136
+ def step(from, symbol)
137
+ from = walking_to_from(from)
138
+ from.collect{|s| s.step(symbol)}.flatten.uniq
139
+ end
140
+
141
+ #
142
+ # Returns the state reached from _from_ states with an input _symbol_. Returns
143
+ # nil or the empty array (according to _from_ conventions) if no state can be
144
+ # reached with the given symbol.
145
+ #
146
+ def dfa_step(from, symbol)
147
+ step = walking_to_from(from).collect{|s| s.dfa_step(symbol)}.flatten.uniq
148
+ walking_to_dfa_result(step, from)
149
+ end
150
+
151
+ #
152
+ # Computes an array representing the set of states that can be reached from
153
+ # _from_ states with the given input _symbol_.
154
+ #
155
+ # This method is epsilon symbol aware (represented with nil) on non
156
+ # deterministic automata, meaning that it actually computes the set of
157
+ # reachable states through strings respecting the <tt>eps* symbol eps*</tt>
158
+ # regular expression, where eps is the epsilon symbol.
159
+ #
160
+ def delta(from, symbol)
161
+ walking_to_from(from).collect{|s| s.delta(symbol)}.flatten.uniq
162
+ end
163
+
164
+ #
165
+ # Returns the target state (or the target states, according to _from_
166
+ # conventions) that can be reached from _from_ states with a given input
167
+ # _symbol_. Returns nil (or an empty array, according to the same conventions)
168
+ # if no such state exists.
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)
173
+ end
174
+
175
+ #
176
+ # Splits a given input and returns a triplet <tt>[parsed,reached,remaining]</tt>
177
+ # where _parsed_ is an array of parsed symbols, _reached_ is the set of reached
178
+ # states with the _parsed_ input string and _remaining_ is an array of symbols
179
+ # with the unparsable part of the string. This method is epsilon symbol aware.
180
+ #
181
+ # By construction, the following properties are verified:
182
+ # - <tt>parsed + remaining == input</tt> (assuming input is an array of symbols),
183
+ # which means that atring concatenation of parsed and remaining symbols is
184
+ # is the input string.
185
+ # - <tt>reached.empty? == false</tt>, because at least initial states (or
186
+ # _from_ if provided) are reached.
187
+ # - <tt>remaining.empty? == parses?(input,from)</tt>, meaning that the automaton
188
+ # parses the whole input if there is no remaining symol.
189
+ # - <tt>delta(reached, remaining[0]).empty? unless remaining.empty?</tt>,
190
+ # which express the splitting stop condition: splitting continues while at
191
+ # least one state can be reached with the next symbol.
192
+ #
193
+ def split(input, from=nil, sort=false)
194
+ if deterministic?
195
+ parsed, reached, remaining = dfa_split(input, from)
196
+ [parsed, walking_from_dfa_to_nfa_result(reached), remaining]
197
+ else
198
+ # the three elements of the triplet
199
+ parsed = []
200
+ reached = walking_to_from(from)
201
+ remaining = walking_to_modifiable_symbols(input)
202
+
203
+ # walk now
204
+ until remaining.empty?
205
+ symb = remaining[0]
206
+ next_reached = delta(reached, symb)
207
+
208
+ # stop it if no reached state
209
+ break if next_reached.empty?
210
+
211
+ # otherwise, update triplet
212
+ parsed << remaining.shift
213
+ reached = next_reached
214
+ end
215
+ reached.sort! if sort
216
+ [parsed, reached, remaining]
217
+ end
218
+ end
219
+
220
+ # Same as split, respecting dfa conventions.
221
+ 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?
234
+
235
+ # otherwise, update triplet
236
+ parsed << remaining.shift
237
+ reached = next_reached
238
+ end
239
+ [parsed, walking_to_dfa_result(reached, from), remaining]
240
+ end
241
+
242
+ #
243
+ # Walks the automaton with an input string, starting at states _from_,
244
+ # collects the set of all reached states and returns it. Unlike split,
245
+ # <b>returned array is empty if the string is not parsable by the automaton</b>.
246
+ # This method is epsilon symbol aware.
247
+ #
248
+ def reached(input, from=nil)
249
+ parsed, reached, remaining = split(input, from)
250
+ remaining.empty? ? reached : []
251
+ end
252
+
253
+ # Same as reached, respecting dfa conventions.
254
+ def dfa_reached(input, from=nil)
255
+ walking_to_dfa_result(reached(input,from),from)
256
+ end
257
+
258
+ #
259
+ # Checks if the automaton is able to parse an input string. Returns true if
260
+ # at least one state can be reached, false otherwise. Unlike accepts?, the
261
+ # labeling of the reached state does not count.
262
+ #
263
+ def parses?(input, from=nil)
264
+ not(reached(input,from).empty?)
265
+ end
266
+
267
+ #
268
+ # Checks if the automaton accepts an input string. Returns true if at least
269
+ # one accepting state can be reached, false otherwise.
270
+ #
271
+ def accepts?(input, from=nil)
272
+ not reached(input,from).select{|s| s.accepting? and not s.error?}.empty?
273
+ end
274
+
275
+ #
276
+ # Checks if the automaton rejects an input string. Returns true if no
277
+ # accepting state can be reached, false otherwise.
278
+ #
279
+ def rejects?(input, from=nil)
280
+ not(accepts?(input, from))
281
+ end
282
+
283
+ # Returns '1' if the string is accepted by the automaton,
284
+ # '0' otherwise.
285
+ def label_of(str)
286
+ accepts?(str) ? '1' : '0'
287
+ end
288
+
289
+ ### protected section ########################################################
290
+ protected
291
+
292
+ #
293
+ # Converts an input to a modifiable array of symbols.
294
+ #
295
+ # If _input_ is an array, it is simply duplicated. If an InputString,
296
+ # InputString#symbols is invoked and result is duplicated. If _input_ is a
297
+ # ruby String, it is split using <tt>input.split(' ')</tt>. Raises an
298
+ # ArgumentError otherwise.
299
+ #
300
+ def walking_to_modifiable_symbols(input)
301
+ case input
302
+ when Array
303
+ input.dup
304
+ when InputString
305
+ input.symbols.dup
306
+ when String
307
+ ADL::parse_string(input).symbols.dup
308
+ else
309
+ raise(ArgumentError,
310
+ "#{input} cannot be converted to a array of symbols", caller)
311
+ end
312
+ end
313
+
314
+ # Implements _from_ conventions.
315
+ def walking_to_from(from)
316
+ return initial_states if from.nil?
317
+ Array===from ? from.collect{|s| to_state(s)} : [to_state(from)]
318
+ end
319
+
320
+ # Implements _return_ conventions of dfa_xxx methods.
321
+ def walking_to_dfa_result(result, from)
322
+ result.compact! # methods are allowed to return [nil]
323
+ Array===from ? result : (result.empty? ? nil : result[0])
324
+ end
325
+
326
+ # Implements _return_ conventions of standard methods that uses dfa_xxx ones.
327
+ def walking_from_dfa_to_nfa_result(result)
328
+ Array===result ? result : (result.nil? ? [] : [result])
329
+ end
330
+
331
+ end # end Walking
332
+ include Stamina::Automaton::Walking
333
+ include Stamina::Classifier
334
+ end # class Automaton
335
+ end # end Stamina
336
+
@@ -0,0 +1,37 @@
1
+ module Stamina
2
+ #
3
+ # Provides a reusable module for binary classifiers. Classes including this
4
+ # module are required to provide a label_of(string) method, returning '1' for
5
+ # strings considered positive, and '0' fr strings considered negative.
6
+ #
7
+ # Note that an Automaton being a classifier it already includes this module.
8
+ #
9
+ module Classifier
10
+
11
+ #
12
+ # Computes a signature for a given sample (that is, an ordered set of strings).
13
+ # The signature is a string containing 1 (considered positive, or accepted)
14
+ # and 0 (considered negative, or rejected), one for each string.
15
+ #
16
+ def signature(sample)
17
+ signature = ''
18
+ sample.each do |str|
19
+ signature << label_of(str)
20
+ end
21
+ signature
22
+ end
23
+
24
+ #
25
+ # Checks if a labeled sample is correctly classified by the classifier.
26
+ #
27
+ def correctly_classify?(sample)
28
+ sample.each do |str|
29
+ label = label_of(str)
30
+ expected = (str.positive? ? '1' : '0')
31
+ return false unless expected==label
32
+ end
33
+ true
34
+ end
35
+
36
+ end # module Classifier
37
+ end # module Stamina
@@ -0,0 +1,73 @@
1
+ require 'stamina/command/stamina_command'
2
+ module Stamina
3
+ module Command
4
+
5
+ # Implementation of the adl2dot command line tool
6
+ class Adl2DotCommand < StaminaCommand
7
+
8
+ # Gif output instead of dot?
9
+ attr_reader :gif_output
10
+
11
+ # Creates a score command instance
12
+ def initialize
13
+ super("adl2dot", "[options] automaton.adl",
14
+ "Prints an automaton expressed in ADL in dot (or gif) format")
15
+ @gif_output = false
16
+ end
17
+
18
+ # Installs additional options
19
+ def options
20
+ super do |opt|
21
+ opt.on("-o", "--output=OUTPUT",
22
+ "Flush result output file") do |value|
23
+ assert_writable_file(value)
24
+ @output_file = value
25
+ end
26
+ opt.on("-g", "--gif",
27
+ "Generates a gif file instead of a dot one") do
28
+ @gif_output = true
29
+ end
30
+ end
31
+ end
32
+
33
+ # Sets the automaton file
34
+ def automaton_file=(file)
35
+ assert_readable_file(file)
36
+ @automaton_file = file
37
+ @automaton = Stamina::ADL.parse_automaton_file(file)
38
+ rescue Stamina::ADL::ParseError
39
+ raise ArgumentError, "#{file} is not a valid ADL automaton file"
40
+ end
41
+
42
+ # Returns the output file to use
43
+ def output_file
44
+ @output_file || "#{File.basename(@automaton_file, '.adl')}.#{gif_output ? 'gif' : 'dot'}"
45
+ end
46
+
47
+ # Executes the command
48
+ def main(argv)
49
+ parse(argv, :automaton_file)
50
+
51
+ # create a file for the dot output
52
+ if gif_output
53
+ require 'tempfile'
54
+ dotfile = Tempfile.new("stamina").path
55
+ else
56
+ dotfile = output_file
57
+ end
58
+
59
+ # Flush automaton inside it
60
+ File.open(dotfile, 'w') do |f|
61
+ f << Stamina::ADL.parse_automaton_file(@automaton_file).to_dot
62
+ end
63
+
64
+ # if gif output, use dot to convert it
65
+ if gif_output
66
+ `dot -Tgif -o #{output_file} #{dotfile}`
67
+ end
68
+ end
69
+
70
+ end # class ScoreCommand
71
+
72
+ end # module Command
73
+ end # module Stamina
@@ -0,0 +1,57 @@
1
+ require 'stamina/command/stamina_command'
2
+ require 'stamina/induction/rpni'
3
+ module Stamina
4
+ module Command
5
+
6
+ # Implementation of the classify command line tool
7
+ class ClassifyCommand < StaminaCommand
8
+
9
+ # Creates a score command instance
10
+ def initialize
11
+ super("classify", "[options] sample.adl automaton.adl",
12
+ "Classify a sample using an automaton, both expressed in ADL")
13
+ end
14
+
15
+ # Installs additional options
16
+ def options
17
+ super do |opt|
18
+ opt.on("-o", "--output=OUTPUT",
19
+ "Flush classification signature in output file") do |value|
20
+ assert_writable_file(value)
21
+ @output_file = value
22
+ end
23
+ end
24
+ end
25
+
26
+ # Sets the sample file
27
+ def sample_file=(file)
28
+ assert_readable_file(file)
29
+ @sample = Stamina::ADL.parse_sample_file(file)
30
+ rescue Stamina::ADL::ParseError
31
+ raise ArgumentError, "#{file} is not a valid ADL sample file"
32
+ end
33
+
34
+ # Sets the automaton file
35
+ def automaton_file=(file)
36
+ assert_readable_file(file)
37
+ @automaton = Stamina::ADL.parse_automaton_file(file)
38
+ rescue Stamina::ADL::ParseError
39
+ raise ArgumentError, "#{file} is not a valid ADL automaton file"
40
+ end
41
+
42
+ # Executes the command
43
+ def main(argv)
44
+ parse(argv, :sample_file, :automaton_file)
45
+ if @output_file
46
+ File.open(@output_file, 'w') do |file|
47
+ file << @automaton.signature(@sample) << "\n"
48
+ end
49
+ else
50
+ STDOUT << @automaton.signature(@sample) << "\n"
51
+ end
52
+ end
53
+
54
+ end # class ClassifyCommand
55
+
56
+ end # module Command
57
+ end # module Stamina