wapiti 0.0.1

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 (63) hide show
  1. data/.autotest +13 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE +30 -0
  6. data/README.md +153 -0
  7. data/Rakefile +33 -0
  8. data/ext/wapiti/bcd.c +392 -0
  9. data/ext/wapiti/decoder.c +535 -0
  10. data/ext/wapiti/decoder.h +46 -0
  11. data/ext/wapiti/extconf.rb +8 -0
  12. data/ext/wapiti/gradient.c +818 -0
  13. data/ext/wapiti/gradient.h +81 -0
  14. data/ext/wapiti/lbfgs.c +294 -0
  15. data/ext/wapiti/model.c +296 -0
  16. data/ext/wapiti/model.h +100 -0
  17. data/ext/wapiti/native.c +1238 -0
  18. data/ext/wapiti/native.h +15 -0
  19. data/ext/wapiti/options.c +278 -0
  20. data/ext/wapiti/options.h +91 -0
  21. data/ext/wapiti/pattern.c +395 -0
  22. data/ext/wapiti/pattern.h +56 -0
  23. data/ext/wapiti/progress.c +167 -0
  24. data/ext/wapiti/progress.h +43 -0
  25. data/ext/wapiti/quark.c +272 -0
  26. data/ext/wapiti/quark.h +46 -0
  27. data/ext/wapiti/reader.c +553 -0
  28. data/ext/wapiti/reader.h +73 -0
  29. data/ext/wapiti/rprop.c +191 -0
  30. data/ext/wapiti/sequence.h +148 -0
  31. data/ext/wapiti/sgdl1.c +218 -0
  32. data/ext/wapiti/thread.c +171 -0
  33. data/ext/wapiti/thread.h +42 -0
  34. data/ext/wapiti/tools.c +202 -0
  35. data/ext/wapiti/tools.h +54 -0
  36. data/ext/wapiti/trainers.h +39 -0
  37. data/ext/wapiti/vmath.c +372 -0
  38. data/ext/wapiti/vmath.h +51 -0
  39. data/ext/wapiti/wapiti.c +288 -0
  40. data/ext/wapiti/wapiti.h +45 -0
  41. data/lib/wapiti.rb +30 -0
  42. data/lib/wapiti/errors.rb +17 -0
  43. data/lib/wapiti/model.rb +49 -0
  44. data/lib/wapiti/options.rb +113 -0
  45. data/lib/wapiti/utility.rb +15 -0
  46. data/lib/wapiti/version.rb +3 -0
  47. data/spec/fixtures/ch.mod +18550 -0
  48. data/spec/fixtures/chpattern.txt +52 -0
  49. data/spec/fixtures/chtest.txt +1973 -0
  50. data/spec/fixtures/chtrain.txt +19995 -0
  51. data/spec/fixtures/nppattern.txt +52 -0
  52. data/spec/fixtures/nptest.txt +1973 -0
  53. data/spec/fixtures/nptrain.txt +19995 -0
  54. data/spec/fixtures/pattern.txt +14 -0
  55. data/spec/fixtures/test.txt +60000 -0
  56. data/spec/fixtures/train.txt +1200 -0
  57. data/spec/spec_helper.rb +21 -0
  58. data/spec/wapiti/model_spec.rb +173 -0
  59. data/spec/wapiti/native_spec.rb +12 -0
  60. data/spec/wapiti/options_spec.rb +175 -0
  61. data/spec/wapiti/utility_spec.rb +22 -0
  62. data/wapiti.gemspec +35 -0
  63. metadata +178 -0
@@ -0,0 +1,288 @@
1
+ /*
2
+ * Wapiti - A linear-chain CRF tool
3
+ *
4
+ * Copyright (c) 2009-2011 CNRS
5
+ * All rights reserved.
6
+ *
7
+ * Redistribution and use in source and binary forms, with or without
8
+ * modification, are permitted provided that the following conditions are met:
9
+ * * Redistributions of source code must retain the above copyright
10
+ * notice, this list of conditions and the following disclaimer.
11
+ * * Redistributions in binary form must reproduce the above copyright
12
+ * notice, this list of conditions and the following disclaimer in the
13
+ * documentation and/or other materials provided with the distribution.
14
+ *
15
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ * POSSIBILITY OF SUCH DAMAGE.
26
+ */
27
+ #include <stdbool.h>
28
+ #include <stddef.h>
29
+ #include <stdlib.h>
30
+ #include <stdio.h>
31
+ #include <string.h>
32
+
33
+ #include "decoder.h"
34
+ #include "model.h"
35
+ #include "options.h"
36
+ #include "progress.h"
37
+ #include "quark.h"
38
+ #include "reader.h"
39
+ #include "sequence.h"
40
+ #include "tools.h"
41
+ #include "trainers.h"
42
+ #include "wapiti.h"
43
+
44
+ /*******************************************************************************
45
+ * Training
46
+ ******************************************************************************/
47
+ static void trn_auto(mdl_t *mdl) {
48
+ const int maxiter = mdl->opt->maxiter;
49
+ mdl->opt->maxiter = 3;
50
+ trn_sgdl1(mdl);
51
+ mdl->opt->maxiter = maxiter;
52
+ trn_lbfgs(mdl);
53
+ }
54
+
55
+ static const struct {
56
+ char *name;
57
+ void (* train)(mdl_t *mdl);
58
+ } trn_lst[] = {
59
+ {"l-bfgs", trn_lbfgs},
60
+ {"sgd-l1", trn_sgdl1},
61
+ {"bcd", trn_bcd },
62
+ {"rprop", trn_rprop},
63
+ {"rprop+", trn_rprop},
64
+ {"rprop-", trn_rprop},
65
+ {"auto", trn_auto }
66
+ };
67
+ static const int trn_cnt = sizeof(trn_lst) / sizeof(trn_lst[0]);
68
+
69
+ void dotrain(mdl_t *mdl) {
70
+ // Check if the user requested the trainer list. If this is not the
71
+ // case, search the trainer.
72
+ if (!strcmp(mdl->opt->algo, "list")) {
73
+ info("Available training algorithms:\n");
74
+ for (int i = 0; i < trn_cnt; i++)
75
+ info("\t%s\n", trn_lst[i].name);
76
+ exit(EXIT_SUCCESS);
77
+ }
78
+ int trn;
79
+ for (trn = 0; trn < trn_cnt; trn++)
80
+ if (!strcmp(mdl->opt->algo, trn_lst[trn].name))
81
+ break;
82
+ if (trn == trn_cnt)
83
+ fatal("unknown algorithm '%s'", mdl->opt->algo);
84
+ // Load a previous model to train again if specified by the user.
85
+ if (mdl->opt->model != NULL) {
86
+ info("* Load previous model\n");
87
+ FILE *file = fopen(mdl->opt->model, "r");
88
+ if (file == NULL)
89
+ pfatal("cannot open input model file");
90
+ mdl_load(mdl, file);
91
+ }
92
+ // Load the pattern file. This will unlock the database if previously
93
+ // locked by loading a model.
94
+ if (mdl->opt->pattern != NULL) {
95
+ info("* Load patterns\n");
96
+ FILE *file = fopen(mdl->opt->pattern, "r");
97
+ if (file == NULL)
98
+ pfatal("cannot open pattern file");
99
+ rdr_loadpat(mdl->reader, file);
100
+ fclose(file);
101
+ qrk_lock(mdl->reader->obs, false);
102
+ }
103
+ // Load the training data. When this is done we lock the quarks as we
104
+ // don't want to put in the model, informations present only in the
105
+ // devlopment set.
106
+ info("* Load training data\n");
107
+ FILE *file = stdin;
108
+ if (mdl->opt->input != NULL) {
109
+ file = fopen(mdl->opt->input, "r");
110
+ if (file == NULL)
111
+ pfatal("cannot open input data file");
112
+ }
113
+ mdl->train = rdr_readdat(mdl->reader, file, true);
114
+ if (mdl->opt->input != NULL)
115
+ fclose(file);
116
+ qrk_lock(mdl->reader->lbl, true);
117
+ qrk_lock(mdl->reader->obs, true);
118
+ if (mdl->train == NULL || mdl->train->nseq == 0)
119
+ fatal("no train data loaded");
120
+ // If present, load the development set in the model. If not specified,
121
+ // the training dataset will be used instead.
122
+ if (mdl->opt->devel != NULL) {
123
+ info("* Load development data\n");
124
+ FILE *file = fopen(mdl->opt->devel, "r");
125
+ if (file == NULL)
126
+ pfatal("cannot open development file");
127
+ mdl->devel = rdr_readdat(mdl->reader, file, true);
128
+ fclose(file);
129
+ }
130
+ // Initialize the model. If a previous model was loaded, this will be
131
+ // just a resync, else the model structure will be created.
132
+ if (mdl->theta == NULL)
133
+ info("* Initialize the model\n");
134
+ else
135
+ info("* Resync the model\n");
136
+ mdl_sync(mdl);
137
+ // Display some statistics as we all love this.
138
+ info("* Summary\n");
139
+ info(" nb train: %d\n", mdl->train->nseq);
140
+ if (mdl->devel != NULL)
141
+ info(" nb devel: %d\n", mdl->devel->nseq);
142
+ info(" nb labels: %zu\n", mdl->nlbl);
143
+ info(" nb blocks: %zu\n", mdl->nobs);
144
+ info(" nb features: %zu\n", mdl->nftr);
145
+ // And train the model...
146
+ info("* Train the model with %s\n", mdl->opt->algo);
147
+ uit_setup(mdl);
148
+ trn_lst[trn].train(mdl);
149
+ uit_cleanup(mdl);
150
+ // If requested compact the model.
151
+ if (mdl->opt->compact) {
152
+ const size_t O = mdl->nobs;
153
+ const size_t F = mdl->nftr;
154
+ info("* Compacting the model\n");
155
+ mdl_compact(mdl);
156
+ info(" %8zu observations removed\n", O - mdl->nobs);
157
+ info(" %8zu features removed\n", F - mdl->nftr);
158
+ }
159
+ // And save the trained model
160
+ info("* Save the model\n");
161
+ file = stdout;
162
+ if (mdl->opt->output != NULL) {
163
+ file = fopen(mdl->opt->output, "w");
164
+ if (file == NULL)
165
+ pfatal("cannot open output model");
166
+ }
167
+ mdl_save(mdl, file);
168
+ if (mdl->opt->output != NULL)
169
+ fclose(file);
170
+ info("* Done\n");
171
+ }
172
+
173
+ /*******************************************************************************
174
+ * Labeling
175
+ ******************************************************************************/
176
+ void dolabel(mdl_t *mdl) {
177
+ // First, load the model provided by the user. This is mandatory to
178
+ // label new datas ;-)
179
+ if (mdl->opt->model == NULL)
180
+ fatal("you must specify a model");
181
+ info("* Load model\n");
182
+ FILE *file = fopen(mdl->opt->model, "r");
183
+ if (file == NULL)
184
+ pfatal("cannot open input model file");
185
+ mdl_load(mdl, file);
186
+ // Open input and output files
187
+ FILE *fin = stdin, *fout = stdout;
188
+ if (mdl->opt->input != NULL) {
189
+ fin = fopen(mdl->opt->input, "r");
190
+ if (fin == NULL)
191
+ pfatal("cannot open input data file");
192
+ }
193
+ if (mdl->opt->output != NULL) {
194
+ fout = fopen(mdl->opt->output, "w");
195
+ if (fout == NULL)
196
+ pfatal("cannot open output data file");
197
+ }
198
+ // Do the labelling
199
+ info("* Label sequences\n");
200
+ tag_label(mdl, fin, fout);
201
+ info("* Done\n");
202
+ // And close files
203
+ if (mdl->opt->input != NULL)
204
+ fclose(fin);
205
+ if (mdl->opt->output != NULL)
206
+ fclose(fout);
207
+ }
208
+
209
+ /*******************************************************************************
210
+ * Dumping
211
+ ******************************************************************************/
212
+ void dodump(mdl_t *mdl) {
213
+ // Load input model file
214
+ info("* Load model\n");
215
+ FILE *fin = stdin;
216
+ if (mdl->opt->input != NULL) {
217
+ fin = fopen(mdl->opt->input, "r");
218
+ if (fin == NULL)
219
+ pfatal("cannot open input data file");
220
+ }
221
+ mdl_load(mdl, fin);
222
+ if (mdl->opt->input != NULL)
223
+ fclose(fin);
224
+ // Open output file
225
+ FILE *fout = stdout;
226
+ if (mdl->opt->output != NULL) {
227
+ fout = fopen(mdl->opt->output, "w");
228
+ if (fout == NULL)
229
+ pfatal("cannot open output data file");
230
+ }
231
+ // Dump model
232
+ info("* Dump model\n");
233
+ const size_t Y = mdl->nlbl;
234
+ const size_t O = mdl->nobs;
235
+ const qrk_t *Qlbl = mdl->reader->lbl;
236
+ const qrk_t *Qobs = mdl->reader->obs;
237
+ for (size_t o = 0; o < O; o++) {
238
+ const char *obs = qrk_id2str(Qobs, o);
239
+ bool empty = true;
240
+ if (mdl->kind[o] & 1) {
241
+ const double *w = mdl->theta + mdl->uoff[o];
242
+ for (size_t y = 0; y < Y; y++) {
243
+ if (w[y] == 0.0)
244
+ continue;
245
+ const char *ly = qrk_id2str(Qlbl, y);
246
+ fprintf(fout, "%s\t#\t%s\t%f\n", obs, ly, w[y]);
247
+ empty = false;
248
+ }
249
+ }
250
+ if (mdl->kind[o] & 2) {
251
+ const double *w = mdl->theta + mdl->boff[o];
252
+ for (size_t d = 0; d < Y * Y; d++) {
253
+ if (w[d] == 0.0)
254
+ continue;
255
+ const char *ly = qrk_id2str(Qlbl, d % Y);
256
+ const char *lyp = qrk_id2str(Qlbl, d / Y);
257
+ fprintf(fout, "%s\t%s\t%s\t%f\n", obs, lyp, ly,
258
+ w[d]);
259
+ empty = false;
260
+ }
261
+ }
262
+ if (!empty)
263
+ fprintf(fout, "\n");
264
+ }
265
+ if (mdl->opt->output != NULL)
266
+ fclose(fout);
267
+ }
268
+
269
+ /*******************************************************************************
270
+ * Entry point
271
+ ******************************************************************************/
272
+ int wapiti_main(int argc, char *argv[argc]) {
273
+ // We first parse command line switchs
274
+ opt_t opt = opt_defaults;
275
+ opt_parse(argc, argv, &opt);
276
+ // Next we prepare the model
277
+ mdl_t *mdl = mdl_new(rdr_new(opt.maxent));
278
+ mdl->opt = &opt;
279
+ // And switch to requested mode
280
+ switch (opt.mode) {
281
+ case 0: dotrain(mdl); break;
282
+ case 1: dolabel(mdl); break;
283
+ case 2: dodump(mdl); break;
284
+ }
285
+ // And cleanup
286
+ mdl_free(mdl);
287
+ return EXIT_SUCCESS;
288
+ }
@@ -0,0 +1,45 @@
1
+ /*
2
+ * Wapiti - A linear-chain CRF tool
3
+ *
4
+ * Copyright (c) 2009-2011 CNRS
5
+ * All rights reserved.
6
+ *
7
+ * Redistribution and use in source and binary forms, with or without
8
+ * modification, are permitted provided that the following conditions are met:
9
+ * * Redistributions of source code must retain the above copyright
10
+ * notice, this list of conditions and the following disclaimer.
11
+ * * Redistributions in binary form must reproduce the above copyright
12
+ * notice, this list of conditions and the following disclaimer in the
13
+ * documentation and/or other materials provided with the distribution.
14
+ *
15
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ * POSSIBILITY OF SUCH DAMAGE.
26
+ */
27
+ #ifndef wapiti_h
28
+ #define wapiti_h
29
+
30
+ #define VERSION "1.2.0"
31
+
32
+ /* XVM_ANSI:
33
+ * By uncomenting the following define, you can force wapiti to not use SSE2
34
+ * even if available.
35
+ */
36
+ //#define XVM_ANSI
37
+
38
+ /* MTH_ANSI:
39
+ * By uncomenting the following define, you can disable the use of POSIX
40
+ * threads in the multi-threading part of Wapiti, for non-POSIX systems.
41
+ */
42
+ //#define MTH_ANSI
43
+
44
+ #endif
45
+
@@ -0,0 +1,30 @@
1
+
2
+ require 'logger'
3
+ require 'tempfile'
4
+
5
+ require 'wapiti/version'
6
+
7
+ module Wapiti
8
+
9
+ Logger = ::Logger.new(STDOUT)
10
+ Logger.level = ::Logger::WARN
11
+
12
+ class << self
13
+ def log
14
+ Logger
15
+ end
16
+
17
+ def debug!
18
+ log.level == ::Logger::DEBUG
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ require 'wapiti/errors'
25
+ require 'wapiti/native'
26
+
27
+ require 'wapiti/options'
28
+ require 'wapiti/model'
29
+
30
+ require 'wapiti/utility'
@@ -0,0 +1,17 @@
1
+ module Wapiti
2
+
3
+ class Error < StandardError
4
+
5
+ attr_accessor :original
6
+
7
+ def initialize(message = '', original = $!)
8
+ super(message)
9
+ @original = original
10
+ end
11
+
12
+ end
13
+
14
+ class NativeError < Error; end
15
+ class ConfigurationError < Error; end
16
+
17
+ end
@@ -0,0 +1,49 @@
1
+ module Wapiti
2
+
3
+ class Model
4
+
5
+ class << self
6
+
7
+ def train(data, options, &block)
8
+ config = Options.new(options, &block)
9
+
10
+ # check configuration
11
+ if config.pattern.empty?
12
+ raise ConfigurationError, 'invalid options: no pattern specified'
13
+ end
14
+
15
+ unless config.valid?
16
+ raise ConfigurationError, "invalid options: #{ config.validate.join('; ') }"
17
+ end
18
+
19
+ new(config).train(data)
20
+ end
21
+
22
+ def load(filename)
23
+ m = new
24
+ m.path = filename
25
+ m.load
26
+ m
27
+ end
28
+
29
+ end
30
+
31
+ attr_accessor :path
32
+
33
+ def pattern
34
+ options.pattern
35
+ end
36
+
37
+ def pattern=(filename)
38
+ options.pattern = filename
39
+ end
40
+
41
+ private
42
+
43
+ def tokenize(input)
44
+ input
45
+ end
46
+
47
+ end
48
+
49
+ end
@@ -0,0 +1,113 @@
1
+ module Wapiti
2
+
3
+ class Options
4
+
5
+ include Comparable
6
+
7
+ class << self
8
+
9
+ # Returns a sorted list of available option attributes.
10
+ def attribute_names
11
+ @attribute_names ||= %w{ stop_window convergence_window posterior
12
+ max_iterations jobsize threads rho1 rho2 stop_epsilon score check
13
+ algorithm pattern development_data maxent compact sparse label
14
+ }.sort.map(&:to_sym).freeze
15
+ end
16
+
17
+ # Returns the default options.
18
+ def defaults
19
+ @defaults ||= new.attributes
20
+ end
21
+
22
+ # Returns the list of supported algorithm options.
23
+ def algorithms
24
+ @algorithms ||= %w{ l-bfgs sgd-l1 bcd rprop rprop+ rprop- auto }.freeze
25
+ end
26
+
27
+ end
28
+
29
+ # Returns the value of the attribute identified by +name+ or nil
30
+ # if there is no such attribute.
31
+ def [](name)
32
+ has_attribute?(name) ? send(name) : nil
33
+ end
34
+
35
+ # Updates the value of the attribute identified by +name+ with the
36
+ # passed-in +value+.
37
+ def []=(name, value)
38
+ raise ArgumentError, "bad attribute name: #{name}" unless has_attribute?(name)
39
+ send("#{name}=", value)
40
+ end
41
+
42
+ # Updates all the attributes from the passed-in hash.
43
+ def update(attributes = {})
44
+ attributes.each_pair do |k,v|
45
+ mid = "#{k}="
46
+ send(mid, v) if respond_to?(mid)
47
+ end
48
+ self
49
+ end
50
+
51
+ alias update_attributes update
52
+
53
+ def lbfgs
54
+ { :clip => clip, :histsz => histsz, :maxls => maxls }
55
+ end
56
+
57
+ def sgdl1
58
+ { :eta0 => eta0, :alpha => alpha }
59
+ end
60
+
61
+ def bcd
62
+ { :kappa => kappa }
63
+ end
64
+
65
+ def rprop
66
+ {
67
+ :stpmin => stpmin, :stpmax => stpmax, :stpinc => stpinc,
68
+ :stpdec => stpdec, :cutoff => cutoff
69
+ }
70
+ end
71
+
72
+ # Returns a hash of all the attributes with their names and values.
73
+ def attributes
74
+ Hash[*Options.attribute_names.map { |a| [a, send(a)] }.flatten]
75
+ end
76
+
77
+ alias to_hash attributes
78
+
79
+ def has_attribute?(attribute)
80
+ Options.attribute_names.include?(attribute)
81
+ end
82
+
83
+ def valid_algorithm?
84
+ self.class.algorithms.include?(algorithm)
85
+ end
86
+
87
+ def valid?
88
+ validate.empty?
89
+ end
90
+
91
+ def validate
92
+ e = []
93
+
94
+ %w{ threads jobsize alpha histsz maxls eta0 alpha nbest }.each do |name|
95
+ e << "invalid value for #{name}: #{send(name)}" unless send(name) > 0
96
+ end
97
+
98
+ %w{ rho1 rho2 }.each do |name|
99
+ e << "invalid value for #{name}: #{send(name)}" unless send(name) >= 0.0
100
+ end
101
+
102
+ e << "unknown algorithm: #{algorithm}" unless valid_algorithm?
103
+ e << "BCD not supported for training maxent models" if maxent && algorithm == 'bcd'
104
+ e
105
+ end
106
+
107
+ def <=>(other)
108
+ other.respond_to?(:attributes) ? attributes <=> other.attributes : nil
109
+ end
110
+
111
+ end
112
+
113
+ end