answer-factory 0.1.3.4 → 0.1.3.5

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.3.4
1
+ 0.1.3.5
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{answer-factory}
8
- s.version = "0.1.3.4"
8
+ s.version = "0.1.3.5"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Bill Tozier", "Trek Glowacki", "Jesse Sielaff"]
12
- s.date = %q{2010-05-27}
12
+ s.date = %q{2010-06-02}
13
13
  s.default_executable = %q{answer-factory}
14
14
  s.description = %q{The pragmaticgp gem provides a simple framework for building, running and managing genetic programming experiments which automatically discover algorithms and equations to solve user-defined problems.}
15
15
  s.email = %q{bill@vagueinnovation.com}
@@ -36,6 +36,7 @@ Gem::Specification.new do |s|
36
36
  "lib/machines/evaluate_simple_score.rb",
37
37
  "lib/machines/evaluate_with_test_cases.rb",
38
38
  "lib/machines/infrastructure.rb",
39
+ "lib/machines/mutate_codeblock.rb",
39
40
  "lib/machines/mutate_footnotes.rb",
40
41
  "lib/machines/select_by_summed_rank.rb",
41
42
  "lib/machines/select_nondominated.rb",
@@ -51,6 +52,7 @@ Gem::Specification.new do |s|
51
52
  "spec/machines/evaluate_simple_score_spec.rb",
52
53
  "spec/machines/evaluate_with_test_cases_spec.rb",
53
54
  "spec/machines/infrastructure_spec.rb",
55
+ "spec/machines/mutate_codeblock_spec.rb",
54
56
  "spec/machines/mutate_footnotes_spec.rb",
55
57
  "spec/machines/select_by_summed_rank_spec.rb",
56
58
  "spec/machines/select_nondominated_spec.rb",
@@ -75,6 +77,7 @@ Gem::Specification.new do |s|
75
77
  "spec/machines/evaluate_simple_score_spec.rb",
76
78
  "spec/machines/evaluate_with_test_cases_spec.rb",
77
79
  "spec/machines/infrastructure_spec.rb",
80
+ "spec/machines/mutate_codeblock_spec.rb",
78
81
  "spec/machines/mutate_footnotes_spec.rb",
79
82
  "spec/machines/select_by_summed_rank_spec.rb",
80
83
  "spec/machines/select_nondominated_spec.rb",
@@ -16,10 +16,12 @@ require 'machines/evaluate_simple_score'
16
16
  require 'machines/evaluate_with_test_cases'
17
17
  require 'machines/select_nondominated'
18
18
  require 'machines/select_by_summed_rank'
19
+ require 'machines/mutate_codeblock'
19
20
  require 'machines/mutate_footnotes'
20
21
 
21
22
 
22
23
 
24
+
23
25
  require 'factories/factory'
24
26
  require 'factories/workstation'
25
27
 
@@ -113,6 +113,7 @@ module AnswerFactory
113
113
  end
114
114
  end
115
115
 
116
+
116
117
  def move_to(where)
117
118
  raise ArgumentError, "#{where} is not a Symbol" unless where.kind_of?(Symbol)
118
119
  @location = where.to_sym
@@ -14,15 +14,13 @@ module AnswerFactory
14
14
  @name = options[:name] || "evaluator"
15
15
  @sensors = options[:sensors] || {}
16
16
  @csv_filename = options[:training_data_csv]
17
-
18
- self.load_training_data!
19
17
  end
20
18
 
21
19
 
22
20
  def score(batch, overridden_options = {})
23
21
  all_options = @options.merge(overridden_options)
24
- name = all_options[:name]
25
22
 
23
+ score_name = all_options[:name]
26
24
 
27
25
  raise ArgumentError, "EvaluateWithTestCases#score cannot process a #{batch.class}" unless
28
26
  batch.kind_of?(Batch)
@@ -33,6 +31,7 @@ module AnswerFactory
33
31
 
34
32
  batch.each do |answer|
35
33
 
34
+
36
35
  raw_results = Hash.new {|hash, key| hash[key] = []}
37
36
 
38
37
  @test_cases.each do |t|
@@ -54,13 +53,12 @@ module AnswerFactory
54
53
  end
55
54
 
56
55
  @sensors.each do |sensor_name, sensor_block|
57
- answer.scores[sensor_name] = raw_results[sensor_name].inject(0) do |sum, measurement|
58
- sum + measurement.abs
59
- end
56
+ answer.scores["#{score_name}_#{sensor_name}".to_sym] =
57
+ raw_results[sensor_name].inject(0) do |sum, measurement|
58
+ sum + measurement.abs
59
+ end
60
60
  end
61
61
  end
62
-
63
-
64
62
  return batch
65
63
  end
66
64
 
@@ -87,24 +85,28 @@ module AnswerFactory
87
85
  end
88
86
 
89
87
 
88
+ def save_view_doc!
89
+ db = CouchRest.database!(training_datasource)
90
+ db.save_doc({'_id' => "_design/#{@name}",
91
+ views: { test_cases: { map: "function(doc) { emit(doc._id, doc); }"}}})
92
+ end
93
+
94
+
90
95
  def install_training_data_from_csv(csv_filename = @csv_filename)
91
- reader = CSV.new(File.open(csv_filename), headers: true)
92
- reader.readline
93
- split_point = reader.headers.find_index(nil)
94
96
 
95
- input_headers = reader.headers[0...split_point].collect {|head| header_prep(head)}
96
- output_headers = reader.headers[split_point+1..-1].collect {|head| head.strip}
97
+ reader = CSV.new(File.open(csv_filename), headers: true)
98
+ headers = csv_headers(reader)
97
99
 
98
- reader.rewind
100
+ save_view_doc!
99
101
 
100
- offset = input_headers.length+1
102
+ offset = headers[:input_headers].length+1
101
103
  db = CouchRest.database!(training_datasource)
102
104
 
103
105
  reader.each do |row|
104
106
  inputs = {}
105
- input_headers.each_with_index {|header,i| inputs[header] = row[i].strip}
107
+ headers[:input_headers].each_with_index {|header,i| inputs[header] = row[i].strip}
106
108
  outputs = {}
107
- output_headers.each_with_index {|header,i| outputs[header] = row[i+offset].strip}
109
+ headers[:output_headers].each_with_index {|header,i| outputs[header] = row[i+offset].strip}
108
110
  db.bulk_save_doc( {:inputs => inputs, :outputs => outputs})
109
111
  end
110
112
 
@@ -113,6 +115,16 @@ module AnswerFactory
113
115
  end
114
116
 
115
117
 
118
+ def csv_headers(csv_reader)
119
+ csv_reader.readline
120
+ split_point = csv_reader.headers.find_index(nil)
121
+ input_headers = csv_reader.headers[0...split_point].collect {|head| header_prep(head)}
122
+ output_headers = csv_reader.headers[split_point+1..-1].collect {|head| head.strip}
123
+ csv_reader.rewind
124
+ return {input_headers:input_headers, output_headers:output_headers}
125
+ end
126
+
127
+
116
128
  def load_training_data!
117
129
  db = CouchRest.database!(training_datasource)
118
130
  result = db.view("#{@name}/test_cases")
@@ -0,0 +1,42 @@
1
+ #encoding: utf-8
2
+ module AnswerFactory
3
+ module Machines
4
+
5
+
6
+
7
+
8
+ class MutateCodeblock < Machine
9
+
10
+ def build(batch, overridden_options={})
11
+ raise ArgumentError, "MutateCodeblock#build cannot process a #{batch.class}" unless
12
+ batch.kind_of?(Batch)
13
+
14
+ all_options = @options.merge(overridden_options)
15
+
16
+ replicates = all_options[:replicates] || 1
17
+
18
+ result = Batch.new
19
+
20
+ batch.each do |answer|
21
+ replicates.times do
22
+ which_point = rand(answer.points) + 1
23
+ size = all_options[:size_preserving?] ?
24
+ answer.program[which_point].points :
25
+ Kernel.rand(2 * answer.program[which_point].points) + 1
26
+ new_code = NudgeType::CodeType.any_value(all_options.merge({target_size_in_points:size}))
27
+ mutated_code = answer.replace_point_or_clone(which_point, new_code)
28
+ variant = Answer.new(mutated_code, progress:answer.progress + 1)
29
+ result << variant
30
+ end
31
+ end
32
+
33
+ return result
34
+ end
35
+
36
+
37
+ alias :generate :build
38
+
39
+
40
+ end
41
+ end
42
+ end
@@ -42,18 +42,19 @@ describe "CouchDB stuff" do
42
42
 
43
43
  describe "Machine CouchDB interaction" do
44
44
  before(:each) do
45
- the_database_name = "#{@db_uri}_training"
46
- @db = CouchRest.database!(the_database_name)
45
+ @db_uri = "#{configatron.factory.couchdb.server}/#{configatron.factory.couchdb.name}"
46
+ training_database_name = "#{@db_uri}_training"
47
+ @db = CouchRest.database!(training_database_name)
47
48
  end
48
49
 
49
- it "should accept :install_training_data_from_csv to populate a db from csv" do
50
+ after(:each) do
51
+ @db.delete!
52
+ end
53
+
54
+
55
+ it "should populate the training db from a csv upon :install_training_data_from_csv" do
50
56
  configatron.temp do
51
57
  configatron.factory.training_datasource = "http://127.0.0.1:5984/integration_test_db_training"
52
- @db.save_doc(
53
- {'_id' => "_design/tester",
54
- views: { test_cases: { map:
55
- "function(doc) { emit(doc._id, doc); }"}}})
56
-
57
58
  @my_machine = Machines::EvaluateWithTestCases.new(name: :tester)
58
59
  @my_machine.install_training_data_from_csv('./spec/fixtures/my_data_source.csv')
59
60
  saved = @db.documents
@@ -79,6 +80,7 @@ describe "CouchDB stuff" do
79
80
  configatron.temp do
80
81
  configatron.factory.training_datasource = "http://127.0.0.1:5984/integration_test_db_training"
81
82
  @my_machine = Machines::EvaluateWithTestCases.new(name: :tester)
83
+ @my_machine.install_training_data_from_csv('./spec/fixtures/my_data_source.csv')
82
84
  @my_machine.load_training_data!
83
85
  @my_machine.test_cases[0].inputs["x1:int"].should == "3"
84
86
  end
@@ -56,7 +56,7 @@ describe "EvaluateWithTestCases" do
56
56
 
57
57
  describe "initialization" do
58
58
 
59
- describe "name" do
59
+ describe "machine name" do
60
60
  it "should have a name" do
61
61
  @tester.should respond_to(:name)
62
62
  end
@@ -127,7 +127,7 @@ describe "EvaluateWithTestCases" do
127
127
  it "should register all the sensors" do
128
128
  @tester.sensors = {"y1" => Proc.new{|interpreter| interpreter.peek_value(:int)} }
129
129
  i = Interpreter.new
130
- Interpreter.should_receive(:new).and_return(i)
130
+ Interpreter.stub(:new).and_return(i)
131
131
  i.should_receive(:register_sensor).exactly(1).times
132
132
  @tester.score(@batch)
133
133
  end
@@ -138,11 +138,9 @@ describe "EvaluateWithTestCases" do
138
138
  "do a", {:name=>:tester, :target_size_in_points=>99}).and_return(i)
139
139
  @tester.score(@batch,target_size_in_points:99)
140
140
  end
141
-
142
-
143
-
144
141
  end
145
142
 
143
+
146
144
  describe "install_training_data_from_csv!" do
147
145
  before(:each) do
148
146
  FakeWeb.register_uri(:any,
@@ -152,10 +150,15 @@ describe "EvaluateWithTestCases" do
152
150
  "http://127.0.0.1:5984/dammit_training/_design/tester/_view/test_cases",
153
151
  :body => @canned, :status => [200, "OK"])
154
152
 
153
+
154
+
155
155
  @f1 = Factory.new(name: "dammit")
156
156
  @my_csv = "./spec/fixtures/my_data_source.csv"
157
157
  @m1 = EvaluateWithTestCases.new(name: :tester, training_data_csv: @my_csv)
158
158
  @training_db = "http://127.0.0.1:5984/dammit_training"
159
+ @my_view = {'_id' => "_design/tester",
160
+ views: { test_cases: { map:
161
+ "function(doc) { emit(doc._id, doc); }"}}}
159
162
 
160
163
  end
161
164
 
@@ -166,22 +169,25 @@ describe "EvaluateWithTestCases" do
166
169
  end
167
170
 
168
171
  it "should open a csv file" do
172
+ @m1.stub(:save_view_doc!) # we're just checking the file is touched
169
173
  f = File.open(@my_csv)
170
- File.should_receive(:open).and_return(f)
174
+ File.stub(:open).and_return(f)
171
175
  c = CSV.new(f, headers: true)
172
- CSV.should_receive(:new).with(f, headers: true).and_return(c)
176
+ CSV.stub(:new).with(f, headers: true).and_return(c)
173
177
  @m1.install_training_data_from_csv(@my_csv)
174
178
  end
175
179
 
176
180
  it "should be the training_data default db" do
181
+ @m1.stub(:save_view_doc!) # we're just checking the filename
177
182
  db = CouchRest.database!(@training_db)
178
- CouchRest.should_receive(:database!).with(@training_db).and_return(db)
183
+ CouchRest.stub(:database!).with(@training_db).and_return(db)
179
184
  @m1.install_training_data_from_csv(@my_csv)
180
185
  end
181
186
 
182
187
  it "makes one doc for every row" do
188
+ @m1.stub(:save_view_doc!) # we're just checking data row saving
183
189
  db = CouchRest.database!(@training_db)
184
- CouchRest.should_receive(:database!).with(@training_db).and_return(db)
190
+ CouchRest.stub(:database!).with(@training_db).and_return(db)
185
191
  db.should_receive(:bulk_save_doc).exactly(3).times
186
192
  @m1.install_training_data_from_csv(@my_csv)
187
193
  end
@@ -194,8 +200,18 @@ describe "EvaluateWithTestCases" do
194
200
  lambda{@m1.header_prep(":int")}.should raise_error ArgumentError
195
201
  end
196
202
 
203
+ it "should create the appropriate view document" do
204
+ db = CouchRest.database!(@training_db)
205
+ CouchRest.stub(:database!).with(@training_db).and_return(db)
206
+ db.stub(:bulk_save_doc)
207
+ db.should_receive(:save_doc).exactly(1).times.with(@my_view)
208
+ @m1.install_training_data_from_csv(@my_csv)
209
+ end
210
+
197
211
  end
198
212
 
213
+
214
+
199
215
  describe "load_training_data!" do
200
216
 
201
217
  before(:each) do
@@ -220,7 +236,7 @@ describe "EvaluateWithTestCases" do
220
236
  "http://127.0.0.1:5984/dammit_training/_design/tester/_view/test_cases",
221
237
  :body => @canned, :status => [200, "OK"])
222
238
  db = CouchRest.database!(@m1.training_datasource)
223
- CouchRest.should_receive(:database!).and_return(db)
239
+ CouchRest.stub(:database!).and_return(db)
224
240
  db.should_receive(:view).with(@design_doc).and_return(@expected)
225
241
  @m1.load_training_data!
226
242
  end
@@ -229,14 +245,14 @@ describe "EvaluateWithTestCases" do
229
245
 
230
246
  it "should ask for the view document" do
231
247
  db = CouchRest.database!(@m1.training_datasource)
232
- CouchRest.should_receive(:database!).and_return(db)
248
+ CouchRest.stub(:database!).and_return(db)
233
249
  db.should_receive(:view).with(@design_doc).and_return(@expected)
234
250
  @m1.load_training_data!
235
251
  end
236
252
 
237
253
  it "should store Array of TestCases in @test_cases" do
238
254
  db = CouchRest.database!(@m1.training_datasource)
239
- CouchRest.should_receive(:database!).and_return(db)
255
+ CouchRest.stub(:database!).and_return(db)
240
256
  db.should_receive(:view).with(@design_doc).and_return(@expected)
241
257
 
242
258
  @m1.load_training_data!
@@ -245,6 +261,7 @@ describe "EvaluateWithTestCases" do
245
261
  end
246
262
  end
247
263
 
264
+
248
265
  describe "scoring" do
249
266
  before(:each) do
250
267
  @m1 = EvaluateWithTestCases.new(name: :tester)
@@ -266,28 +283,35 @@ describe "EvaluateWithTestCases" do
266
283
 
267
284
  it "should run all the Interpreters" do
268
285
  @m1.stub!(:load_training_data!)
269
- Interpreter.should_receive(:new).exactly(10).times.and_return(@i1)
286
+ Interpreter.stub(:new).exactly(10).times.and_return(@i1)
270
287
  @i1.should_receive(:run).at_least(1).times.and_return({})
271
288
  @m1.score(@batch)
272
289
  end
273
290
 
274
291
  it "should register its sensors before each Interpreter run" do
275
- Interpreter.stub!(:new).and_return(@i1)
292
+ Interpreter.stub(:new).and_return(@i1)
276
293
  @i1.should_receive(:register_sensor).at_least(1).times
277
294
  @m1.score(@batch)
278
295
  end
279
296
 
280
297
  it "should have a score for each sensor" do
281
298
  @m1.score(@batch)
282
- @batch.first.scores["y1"].should_not == nil
283
- @batch.first.scores["y2"].should_not == nil
299
+ @batch.first.scores[:tester_y1].should_not == nil
300
+ @batch.first.scores[:tester_y2].should_not == nil
301
+ end
302
+
303
+ it "should not re-evaluate an Answer's score if :static is true and the score is set" do
304
+ pending
305
+ @m1.score(@batch)
306
+ Interpreter.should_not_receive(:new)
307
+ @m1.score(@batch, static:true)
284
308
  end
285
309
 
286
310
  it "should return sum of absolute errors" do
287
311
  @m1.stub!(:load_training_data!)
288
312
  @m1.score(@batch)
289
- @batch[0].scores["y1"].should == 777+775+773+771+769+767+765+763+761+759
290
- @batch[0].scores["y2"].should == 666+663+660+657+654+651+648+645+642+639
313
+ @batch[0].scores[:tester_y1].should == 777+775+773+771+769+767+765+763+761+759
314
+ @batch[0].scores[:tester_y2].should == 666+663+660+657+654+651+648+645+642+639
291
315
  end
292
316
  end
293
317
 
@@ -0,0 +1,93 @@
1
+ #encoding: utf-8
2
+ require File.join(File.dirname(__FILE__), "./../spec_helper")
3
+
4
+ describe "Machines::MutateCodeblock" do
5
+ before(:each) do
6
+ @twiddler = Machines::MutateCodeblock.new
7
+ @two = Batch.[](
8
+ Answer.new("value «int» \n«int» 1"),
9
+ Answer.new("block { block {value «code»} ref x1 block { block {ref x2}}} \n«code» value «bool» \n«bool» false"))
10
+ end
11
+
12
+
13
+ describe "#build method" do
14
+ it "should respond to :build" do
15
+ @twiddler.should respond_to(:build)
16
+ end
17
+
18
+ it "should respond to :generate as an alias of :build" do
19
+ @twiddler.should respond_to(:generate)
20
+ end
21
+
22
+ it "should raise an error if the argument isn't a Batch" do
23
+ lambda{@twiddler.build(@two)}.should_not raise_error
24
+ lambda{@twiddler.build(99)}.should raise_error
25
+ end
26
+
27
+ it "should produce a Batch" do
28
+ @twiddler.build(@two).should be_a_kind_of(Batch)
29
+ end
30
+
31
+ it "should produce a Batch with a new object_id" do
32
+ @twiddler.build(@two).object_id.should_not == @two.object_id
33
+ end
34
+
35
+ it "should not modify or include any Answer object from its argument Batch" do
36
+ original_ids = @two.collect {|a| a.object_id}
37
+ result_ids = @twiddler.build(@two).collect {|a| a.object_id}
38
+ (original_ids & result_ids).length.should == 0
39
+ end
40
+
41
+ describe ":replicates option" do
42
+ it "should produce a :replicates for each arg Answer, determined by a call option" do
43
+ @twiddler.build(@two, replicates:2).length.should == 2 * @two.length
44
+ end
45
+
46
+ it "should use the initialization :replicates option if there isn't a call option" do
47
+ threefer = Machines::MutateCodeblock.new(replicates:3)
48
+ threefer.build(@two).length.should == 3*@two.length
49
+ end
50
+
51
+ it "should default to replicates:1 if none was set as an option" do
52
+ @twiddler.build(@two).length.should == @two.length
53
+ end
54
+ end
55
+
56
+ it "should increment the :progress attribute of the derived Answers, regardless of other settings" do
57
+ @twiddler.build(@two).each {|a| a.progress.should == 1}
58
+ end
59
+
60
+ describe "mutating code" do
61
+ before(:each) do
62
+ @some_code = ReferencePoint.new(:abcde)
63
+ end
64
+
65
+ it "should replace one program point in each mutant"
66
+
67
+ it "should pick the replaced point with uniform probability" do
68
+ CodeType.should_receive(:any_value).exactly(2).times.and_return(@some_code)
69
+ @twiddler.build(@two)
70
+ end
71
+
72
+ it "should replace a point with one that's the same size if :size_preserving? is true" do
73
+ Kernel.should_not_receive(:rand)
74
+ new_dudes = @twiddler.build(@two, size_preserving?:true)
75
+ new_dudes.each_with_index {|a,idx| a.points.should == @two[idx].points}
76
+ end
77
+
78
+ it "should replace a point with one between 1 and 2xsize if :sizepreserving? is false" do
79
+ Kernel.stub!(:rand).and_return(17)
80
+ CodeType.should_receive(:any_value).at_least(2).times.
81
+ with(hash_including(size_preserving?:false,target_size_in_points:18)).and_return(@some_code)
82
+ @twiddler.build(@two, size_preserving?:false)
83
+ end
84
+
85
+ it "should pass all options through to the any_value call" do
86
+ Kernel.stub!(:rand).and_return(3)
87
+ CodeType.should_receive(:any_value).at_least(2).times.
88
+ with(hash_including(target_size_in_points:4, foo:12)).and_return(@some_code)
89
+ @twiddler.build(@two, foo:12)
90
+ end
91
+ end
92
+ end
93
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: answer-factory
3
3
  version: !ruby/object:Gem::Version
4
- hash: 67
4
+ hash: 65
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
9
  - 3
10
- - 4
11
- version: 0.1.3.4
10
+ - 5
11
+ version: 0.1.3.5
12
12
  platform: ruby
13
13
  authors:
14
14
  - Bill Tozier
@@ -18,7 +18,7 @@ autorequire:
18
18
  bindir: bin
19
19
  cert_chain: []
20
20
 
21
- date: 2010-05-27 00:00:00 -04:00
21
+ date: 2010-06-02 00:00:00 -04:00
22
22
  default_executable: answer-factory
23
23
  dependencies:
24
24
  - !ruby/object:Gem::Dependency
@@ -157,6 +157,7 @@ files:
157
157
  - lib/machines/evaluate_simple_score.rb
158
158
  - lib/machines/evaluate_with_test_cases.rb
159
159
  - lib/machines/infrastructure.rb
160
+ - lib/machines/mutate_codeblock.rb
160
161
  - lib/machines/mutate_footnotes.rb
161
162
  - lib/machines/select_by_summed_rank.rb
162
163
  - lib/machines/select_nondominated.rb
@@ -172,6 +173,7 @@ files:
172
173
  - spec/machines/evaluate_simple_score_spec.rb
173
174
  - spec/machines/evaluate_with_test_cases_spec.rb
174
175
  - spec/machines/infrastructure_spec.rb
176
+ - spec/machines/mutate_codeblock_spec.rb
175
177
  - spec/machines/mutate_footnotes_spec.rb
176
178
  - spec/machines/select_by_summed_rank_spec.rb
177
179
  - spec/machines/select_nondominated_spec.rb
@@ -225,6 +227,7 @@ test_files:
225
227
  - spec/machines/evaluate_simple_score_spec.rb
226
228
  - spec/machines/evaluate_with_test_cases_spec.rb
227
229
  - spec/machines/infrastructure_spec.rb
230
+ - spec/machines/mutate_codeblock_spec.rb
228
231
  - spec/machines/mutate_footnotes_spec.rb
229
232
  - spec/machines/select_by_summed_rank_spec.rb
230
233
  - spec/machines/select_nondominated_spec.rb