tilecoding/lib.rs
1// benchmarks only available on nightly for now
2//#![feature(test)]
3
4use std::collections::HashMap;
5
6// convenience function for hashing a hashable object using the std hashmap's default hasher
7fn base_hash<H>(obj: H) -> usize
8where
9 H: std::hash::Hash,
10{
11 use std::collections::hash_map::DefaultHasher;
12 use std::hash::Hasher;
13
14 let mut hasher = DefaultHasher::new();
15 obj.hash(&mut hasher);
16 hasher.finish() as usize
17}
18
19fn calculate_q_floats(floats: &[f64], num_tilings: usize) -> Vec<isize> {
20 floats
21 .iter()
22 .map(|&x| (x * num_tilings as f64).floor() as isize)
23 .collect::<Vec<isize>>()
24}
25
26fn calculate_coords(tiling: usize, num_tilings: usize, q_floats: &Vec<isize>, ints: &Option<&[isize]>) -> Vec<isize> {
27 let tiling_x2 = tiling as isize * 2;
28 let mut coords = Vec::with_capacity(1 + q_floats.len());
29 coords.push(tiling as isize);
30 let mut b = tiling as isize;
31 for q in q_floats.iter() {
32 coords.push((q + b) / num_tilings as isize);
33 b += tiling_x2;
34 }
35 if let Some(ints) = ints {
36 coords.extend(*ints);
37 }
38
39 coords
40}
41
42fn calculate_coords_wrap(tiling: usize, num_tilings: usize, q_floats: &Vec<isize>, wrap_widths: &[Option<isize>], ints: &Option<&[isize]>) -> Vec<isize> {
43 let tiling_x2 = tiling as isize * 2;
44 let mut coords = Vec::with_capacity(1 + q_floats.len());
45 coords.push(tiling as isize);
46 let mut b = tiling as isize;
47 for (q, width) in q_floats.iter().zip(wrap_widths.iter()) {
48 let c: isize = (q + b % num_tilings as isize) / num_tilings as isize;
49 coords.push(match width {
50 Some(w) => c % w,
51 None => c,
52 });
53 b += tiling_x2;
54 }
55 if let Some(ints) = ints {
56 coords.extend(*ints);
57 }
58
59 coords
60}
61
62/// An index-hash-table, or IHT. It will allow to collect tile indices up to a
63/// certain size, after which collisions will start to occur. The underlying storage
64/// is a HashMap
65pub struct IHT {
66 size: usize,
67 overfull_count: usize,
68 dictionary: HashMap<Vec<isize>, usize>,
69}
70
71impl IHT {
72 /// Create a new IHT with the given size. The `tiles` function will never
73 /// report an index >= this size.
74 pub fn new(size: usize) -> IHT {
75 IHT {
76 size,
77 overfull_count: 0,
78 dictionary: HashMap::with_capacity(size),
79 }
80 }
81
82 fn get_index(&mut self, obj: Vec<isize>) -> usize {
83 // store the count for later use
84 let count = self.dictionary.len();
85
86 // use the entry api on hashmaps to improve performance
87 use std::collections::hash_map::Entry;
88 match self.dictionary.entry(obj) {
89 // if the object already exists in the hashmap, return the index
90 Entry::Occupied(o) => *o.get(),
91
92 Entry::Vacant(v) => {
93 // the object isn't already stored in the dictionary
94 if count >= self.size {
95 // if we're full, allow collisions (keeping track of this fact)
96 self.overfull_count += 1;
97 base_hash(v.into_key()) % self.size
98 } else {
99 // otherwise, just insert into the dictionary and return the result
100 *v.insert(count)
101 }
102 }
103 }
104 }
105
106 fn get_index_read_only(&self, obj: Vec<isize>) -> Option<usize> {
107 self.dictionary.get(&obj).map(|i| *i)
108 }
109
110 /// Convenience function to determine if the IHT is full. If it is, new tilings will result in collisions rather than new indices.
111 pub fn full(&self) -> bool {
112 self.dictionary.len() >= self.size
113 }
114
115 /// Convenience function to determine how full the IHT is. The maximum value will be the IHT size
116 pub fn count(&self) -> usize {
117 self.dictionary.len()
118 }
119
120 /// Convenience function get the size of the IHT, in case you forgot what it was
121 pub fn size(&self) -> usize {
122 self.size
123 }
124
125 /// This function takes a series of floating point and integer values, and encodes them as tile indices using the underlying IHT to deal with collisions.
126 ///
127 /// # Arguments
128 ///
129 /// * `num_tilings`—indicates the number of tile indices to be generated (i.e. the length of the returned `Vec`). This value hould be a power of two greater or equal to four times the number of floats according to the original implementation.
130 /// * `floats`—a list of floating-point numbers to be tiled
131 /// * `ints`—an optional list of integers that will also be tiled; all distinct integers will result in different tilings. In reinforcement learning, discrete actions are often provided here.
132 ///
133 /// # Return Value
134 ///
135 /// The returned `Vec<usize>` is a vector containing exactly `num_tilings` elements, with each member being an index of a tile encoded by the function. Each member will always be >= 0 and <= size - 1.
136 ///
137 /// # Example
138 ///
139 /// ```
140 /// # use tilecoding::IHT;
141 /// // initialize an index-hash-table with size `1024`
142 /// let mut iht = IHT::new(1024);
143 ///
144 /// // find the indices of tiles for the point (x, y) = (3.6, 7.21) using 8 tilings:
145 /// let indices = iht.tiles(8, &[3.6, 7.21], None);
146 ///
147 /// // this is the first time we've used the IHT, so we will get the starting tiles:
148 /// assert_eq!(indices, vec![0, 1, 2, 3, 4, 5, 6, 7]);
149 ///
150 /// // a nearby point:
151 /// let indices = iht.tiles(8, &[3.7, 7.21], None);
152 ///
153 /// // differs by one tile:
154 /// assert_eq!(indices, vec![0, 1, 2, 8, 4, 5, 6, 7]);
155 ///
156 /// // and a point more than one away in any dim
157 /// let indices = iht.tiles(8, &[-37.2, 7.0], None);
158 ///
159 /// // will have all different tiles
160 /// assert_eq!(indices, vec![9, 10, 11, 12, 13, 14, 15, 16]);
161 /// ```
162 pub fn tiles(&mut self, num_tilings: usize, floats: &[f64], ints: Option<&[isize]>) -> Vec<usize> {
163 let q_floats = calculate_q_floats(floats, num_tilings);
164 let mut tiles: Vec<usize> = Vec::with_capacity(num_tilings + ints.unwrap_or(&[]).len());
165
166 for tiling in 0..num_tilings {
167 let coords = calculate_coords(tiling, num_tilings, &q_floats, &ints);
168 tiles.push(self.get_index(coords));
169 }
170
171 tiles
172 }
173
174 /// The same as the `tiles` function, except never insert or generate new indices. If an tiling calculate would result in a new tile, return `None` instead
175 pub fn tiles_read_only(&self, num_tilings: usize, floats: &[f64], ints: Option<&[isize]>) -> Vec<Option<usize>> {
176 let q_floats = calculate_q_floats(floats, num_tilings);
177 let mut tiles: Vec<Option<usize>> = Vec::with_capacity(num_tilings + ints.unwrap_or(&[]).len());
178
179 for tiling in 0..num_tilings {
180 let coords = calculate_coords(tiling, num_tilings, &q_floats, &ints);
181 tiles.push(self.get_index_read_only(coords));
182 }
183
184 tiles
185 }
186
187 /// A wrap-around version of `tiles`, described in the [original implementation](http://www.incompleteideas.net/tiles/tiles3.html#Wrap-around_Versions_).
188 ///
189 /// # Arguments
190 ///
191 /// * `num_tilings`—indicates the number of tile indices to be generated (i.e. the length of the returned `Vec`). This value hould be a power of two greater or equal to four times the number of floats according to the original implementation.
192 /// * `floats`—a list of floating-point numbers to be tiled
193 /// * `wrap_widths`—a list of optional integer wrapping points.
194 /// * `ints`—an optional list of integers that will also be tiled; all distinct integers will result in different tilings. In reinforcement learning, discrete actions are often provided here.
195 ///
196 /// # Return Value
197 ///
198 /// The returned `Vec<usize>` is a vector containing exactly `num_tilings` elements, with each member being an index of a tile encoded by the function. Each member will always be >= 0 and <= size - 1.
199 ///
200 /// # Examples
201 ///
202 /// From the [original implementation](http://www.incompleteideas.net/tiles/tiles3.html#Wrap-around_Versions_):
203 ///
204 /// > The tilings we have discussed so far stretch out to infinity with no need to specify a range for them. This is cool, but sometimes not what you want. Sometimes you want the variables to wrap-around over some range. For example, you may have an angle variable that goes from 0 to 2π and then should wrap around, that is, you would like generalization to occur between angles just greater than 0 and angles that are nearly 2π. To accommodate this, we provide some alternative, wrap-around versions of the tiling routines.
205 /// >
206 /// > These versions take an additional input, wrap_widths, which parallels the float structure (array or list), and which specifies for each float the width of the range over which it wraps. If you don't want a float to wrap, it's wrap_width should be [`None`]. The wrap_width is in the same units as the floats, but should be an integer. This can be confusing, so let's do a simple 1D example. Suppose you have one real input, an angle theta. Theta is originally in radians, that is, in [0,2π), which you would like to wrap around at 2π. Remember that these functions all have their tile boundaries at the integers, so if you passed in theta directly there would be tile boundaries at 0, 1, and 2, i.e., just a few tiles over the whole range, which is probably not what you want. So let's say what we want! Suppose we want tilings with 10 tiles over the [0,2π) range. Then we have to scale theta so that it goes from 0 to 10 (instead of 0 to 2π). One would do this by multiplying theta by 10/2π. With the new scaled theta, the wrapping is over [0,10), for a wrap_width of 10.
207 ///
208 /// Here is the code for the above case, assuming we want 16 tilings over the original [0, 2π) range with a memory size of 512:
209 ///
210 /// ```
211 /// # use tilecoding::IHT;
212 /// # let theta: f64 = 0.0;
213 /// let mut iht = IHT::new(512);
214 /// iht.tiles_wrap(
215 /// 16,
216 /// &[theta * 10. / (2.0 * std::f64::consts::PI)],
217 /// &[Some(10)],
218 /// None
219 /// );
220 /// ```
221 ///
222 /// > Note that the code would be exactly the same if the original range of theta was [-π,+π]. Specifying the complete range of wrapping (rather than just the width) is not necessary for the same reason as we did not need to give a range at all in the previous routines.
223 /// >
224 /// > As another example, suppose you wanted to cover the 2π x 3 rectangular area with 16 tilings, each with a width of generalization one-tenth of the space in each dimension, and with wrap-around in the second dimension but not the first. In rust you would do:
225 ///
226 /// ```
227 /// # use tilecoding::IHT;
228 /// # let x: f64 = 0.0;
229 /// # let y: f64 = 0.0;
230 /// let mut iht = IHT::new(512);
231 /// iht.tiles_wrap(
232 /// 16,
233 /// &[x / (3. * 0.1), y / (2.0 * std::f64::consts::PI * 0.1)],
234 /// &[None, Some(10)],
235 /// None
236 /// );
237 /// ```
238 pub fn tiles_wrap(&mut self, num_tilings: usize, floats: &[f64], wrap_widths: &[Option<isize>], ints: Option<&[isize]>) -> Vec<usize> {
239 let q_floats = calculate_q_floats(floats, num_tilings);
240 let mut tiles: Vec<usize> = Vec::with_capacity(num_tilings + ints.unwrap_or(&[]).len());
241
242 for tiling in 0..num_tilings {
243 let coords = calculate_coords_wrap(tiling, num_tilings, &q_floats, wrap_widths, &ints);
244 tiles.push(self.get_index(coords));
245 }
246
247 tiles
248 }
249
250 /// The read-only version of `tiles_wrap`
251 pub fn tiles_wrap_read_only(&self, num_tilings: usize, floats: &[f64], wrap_widths: &[Option<isize>], ints: Option<&[isize]>) -> Vec<Option<usize>> {
252 let q_floats = calculate_q_floats(floats, num_tilings);
253 let mut tiles: Vec<Option<usize>> = Vec::with_capacity(num_tilings + ints.unwrap_or(&[]).len());
254
255 for tiling in 0..num_tilings {
256 let coords = calculate_coords_wrap(tiling, num_tilings, &q_floats, wrap_widths, &ints);
257 tiles.push(self.get_index_read_only(coords));
258 }
259
260 tiles
261 }
262}
263
264/// This function takes a series of floating point and integer values, and encodes them as tile indices using a provided size. This function is generally reserved for when you have extraordinarily large sizes that are too large for the IHT.
265///
266/// # Arguments
267///
268/// * `size`—the upper bounds of all returned indices
269/// * `num_tilings`—indicates the number of tile indices to be generated (i.e. the length of the returned `Vec`). This value hould be a power of two greater or equal to four times the number of floats according to the original implementation.
270/// * `floats`—a list of floating-point numbers to be tiled
271/// * `ints`—an optional list of integers that will also be tiled; all distinct integers will result in different tilings. In reinforcement learning, discrete actions are often provided here.
272///
273/// # Return Value
274///
275/// The returned `Vec<usize>` is a vector containing exactly `num_tilings` elements, with each member being an index of a tile encoded by the function. Each member will always be >= 0 and <= size - 1.
276///
277/// # Example
278///
279/// ```
280/// # use tilecoding::tiles;
281/// // find the indices of tiles for the point (x, y) = (3.6, 7.21) using 8 tilings and a maximum size of 1024:
282/// let indices = tiles(1024, 8, &[3.6, 7.21], None);
283///
284/// // we get tiles all over the 1024 space as a direct result of the hashing
285/// // instead of the more ordered indices provided by an IHT
286/// assert_eq!(indices, vec![511, 978, 632, 867, 634, 563, 779, 737]);
287///
288/// // a nearby point:
289/// let indices = tiles(1024, 8, &[3.7, 7.21], None);
290///
291/// // differs by one tile:
292/// assert_eq!(indices, vec![511, 978, 632, 987, 634, 563, 779, 737]);
293///
294/// // and a point more than one away in any dim
295/// let indices = tiles(1024, 8, &[-37.2, 7.0], None);
296///
297/// // will have all different tiles
298/// assert_eq!(indices, vec![638, 453, 557, 465, 306, 526, 281, 863]);
299/// ```
300pub fn tiles(size: usize, num_tilings: usize, floats: &[f64], ints: Option<&[isize]>) -> Vec<usize> {
301 let q_floats = calculate_q_floats(floats, num_tilings);
302 let mut tiles: Vec<usize> = Vec::with_capacity(num_tilings + ints.unwrap_or(&[]).len());
303
304 for tiling in 0..num_tilings {
305 let coords = calculate_coords(tiling, num_tilings, &q_floats, &ints);
306 tiles.push(base_hash(coords) % size);
307 }
308
309 tiles
310}
311
312/// A wrap-around version of `tiles`, described in the [original implementation](http://www.incompleteideas.net/tiles/tiles3.html#Wrap-around_Versions_).
313///
314/// # Arguments
315///
316/// * `size`—the upper bounds of all returned indices
317/// * `num_tilings`—indicates the number of tile indices to be generated (i.e. the length of the returned `Vec`). This value hould be a power of two greater or equal to four times the number of floats according to the original implementation.
318/// * `floats`—a list of floating-point numbers to be tiled
319/// * `wrap_widths`—a list of optional integer wrapping points.
320/// * `ints`—an optional list of integers that will also be tiled; all distinct integers will result in different tilings. In reinforcement learning, discrete actions are often provided here.
321///
322/// # Return Value
323///
324/// The returned `Vec<usize>` is a vector containing exactly `num_tilings` elements, with each member being an index of a tile encoded by the function. Each member will always be >= 0 and <= size - 1.
325///
326/// # Examples
327///
328/// From the [original implementation](http://www.incompleteideas.net/tiles/tiles3.html#Wrap-around_Versions_):
329///
330/// > The tilings we have discussed so far stretch out to infinity with no need to specify a range for them. This is cool, but sometimes not what you want. Sometimes you want the variables to wrap-around over some range. For example, you may have an angle variable that goes from 0 to 2π and then should wrap around, that is, you would like generalization to occur between angles just greater than 0 and angles that are nearly 2π. To accommodate this, we provide some alternative, wrap-around versions of the tiling routines.
331/// >
332/// > These versions take an additional input, wrap_widths, which parallels the float structure (array or list), and which specifies for each float the width of the range over which it wraps. If you don't want a float to wrap, it's wrap_width should be [`None`]. The wrap_width is in the same units as the floats, but should be an integer. This can be confusing, so let's do a simple 1D example. Suppose you have one real input, an angle theta. Theta is originally in radians, that is, in [0,2π), which you would like to wrap around at 2π. Remember that these functions all have their tile boundaries at the integers, so if you passed in theta directly there would be tile boundaries at 0, 1, and 2, i.e., just a few tiles over the whole range, which is probably not what you want. So let's say what we want! Suppose we want tilings with 10 tiles over the [0,2π) range. Then we have to scale theta so that it goes from 0 to 10 (instead of 0 to 2π). One would do this by multiplying theta by 10/2π. With the new scaled theta, the wrapping is over [0,10), for a wrap_width of 10.
333///
334/// Here is the code for the above case, assuming we want 16 tilings over the original [0, 2π) range with a memory size of 512:
335///
336/// ```
337/// # use tilecoding::tiles_wrap;
338/// # let theta: f64 = 0.0;
339/// tiles_wrap(
340/// 512,
341/// 16,
342/// &[theta * 10. / (2.0 * std::f64::consts::PI)],
343/// &[Some(10)],
344/// None
345/// );
346/// ```
347///
348/// > Note that the code would be exactly the same if the original range of theta was [-π,+π]. Specifying the complete range of wrapping (rather than just the width) is not necessary for the same reason as we did not need to give a range at all in the previous routines.
349/// >
350/// > As another example, suppose you wanted to cover the 2π x 3 rectangular area with 16 tilings, each with a width of generalization one-tenth of the space in each dimension, and with wrap-around in the second dimension but not the first. In rust you would do:
351///
352/// ```
353/// # use tilecoding::tiles_wrap;
354/// # let x: f64 = 0.0;
355/// # let y: f64 = 0.0;
356/// tiles_wrap(
357/// 512,
358/// 16,
359/// &[x / (3. * 0.1), y / (2.0 * std::f64::consts::PI * 0.1)],
360/// &[None, Some(10)],
361/// None
362/// );
363/// ```
364pub fn tiles_wrap(size: usize, num_tilings: usize, floats: &[f64], wrap_widths: &[Option<isize>], ints: Option<&[isize]>) -> Vec<usize> {
365 let q_floats = calculate_q_floats(floats, num_tilings);
366 let mut tiles: Vec<usize> = Vec::with_capacity(num_tilings + ints.unwrap_or(&[]).len());
367
368 for tiling in 0..num_tilings {
369 let coords = calculate_coords_wrap(tiling, num_tilings, &q_floats, wrap_widths, &ints);
370 tiles.push(base_hash(coords) % size);
371 }
372
373 tiles
374}
375
376#[cfg(test)]
377mod tests {
378 //extern crate test;
379
380 use super::*;
381 //use test::Bencher;
382
383 #[test]
384 fn proper_number_of_tiles() {
385 let mut iht = IHT::new(32);
386 let indices = iht.tiles(8, &[0.0], None);
387 assert_eq!(indices.len(), 8);
388 }
389
390 #[test]
391 fn same_tiles_for_same_coords() {
392 let mut iht = IHT::new(32);
393 let indices_1 = iht.tiles(8, &[0.0], None);
394 let indices_2 = iht.tiles(8, &[0.0], None);
395 let indices_3 = iht.tiles(8, &[0.5], None);
396 let indices_4 = iht.tiles(8, &[0.5], None);
397 let indices_5 = iht.tiles(8, &[1.0], None);
398 let indices_6 = iht.tiles(8, &[1.0], None);
399
400 assert_eq!(indices_1, indices_2);
401 assert_eq!(indices_3, indices_4);
402 assert_eq!(indices_5, indices_6);
403 }
404
405 #[test]
406 fn different_tiles_for_different_coords() {
407 let mut iht = IHT::new(32);
408 let indices_1 = iht.tiles(8, &[0.0], None);
409 let indices_2 = iht.tiles(8, &[0.5], None);
410 let indices_3 = iht.tiles(8, &[1.0], None);
411
412 assert_ne!(indices_1, indices_2);
413 assert_ne!(indices_2, indices_3);
414 assert_ne!(indices_1, indices_3);
415 }
416
417 #[test]
418 fn can_be_negative() {
419 let mut iht = IHT::new(32);
420 let indices = iht.tiles(8, &[-10.0], None);
421 assert_eq!(indices.len(), 8);
422 }
423
424 #[test]
425 fn appropriate_distance() {
426 let mut iht = IHT::new(32);
427 let indices_1 = iht.tiles(4, &[0.0], None);
428 let indices_2 = iht.tiles(4, &[0.125], None);
429 let indices_3 = iht.tiles(4, &[0.25], None);
430
431 assert_eq!(indices_1, indices_2);
432 assert_ne!(indices_1, indices_3);
433 }
434
435 #[test]
436 fn iht_can_collide() {
437 const SIZE: usize = 32;
438 let mut iht = IHT::new(SIZE);
439 for i in 0..(SIZE * 2) {
440 let t = iht.tiles(8, &[i as f64], None);
441 assert_eq!(t.len(), 8);
442 for j in 0..8 {
443 assert!(t[j] < SIZE);
444 }
445 }
446 assert!(iht.full());
447 }
448
449 #[test]
450 fn read_only_works() {
451 let mut iht = IHT::new(32);
452 iht.tiles(4, &[0.0], None);
453 let indices = iht.tiles_read_only(4, &[32.0], None);
454 assert_eq!(indices, vec![None, None, None, None]);
455 }
456
457 #[test]
458 fn wrapping_works() {
459 let mut iht = IHT::new(32);
460 let indices_1 = iht.tiles_wrap(4, &[0.0], &[Some(10)], None);
461 let indices_2 = iht.tiles_wrap(4, &[10.0], &[Some(10)], None);
462
463 assert_eq!(indices_1, indices_2);
464 }
465
466 /*#[bench]
467 fn bench_iht_tile_code_small_single_dimension(b: &mut Bencher) {
468 let mut iht = IHT::new(32);
469 b.iter(|| iht.tiles(8, &[0.0], None));
470 }
471
472 #[bench]
473 fn bench_iht_tile_code_single_dimension(b: &mut Bencher) {
474 let mut iht = IHT::new(2048);
475 b.iter(|| iht.tiles(8, &[0.0], None));
476 }
477
478 #[bench]
479 fn bench_iht_tile_code_small_four_dimensions(b: &mut Bencher) {
480 let mut iht = IHT::new(32);
481 b.iter(|| iht.tiles(8, &[0.0, 1.0, 2.0, 3.0], None));
482 }
483
484 #[bench]
485 fn bench_iht_tile_code_four_dimensions(b: &mut Bencher) {
486 let mut iht = IHT::new(2048);
487 b.iter(|| iht.tiles(8, &[0.0, 1.0, 2.0, 3.0], None));
488 }
489
490 #[bench]
491 fn bench_non_iht_tile_code_small_single_dimension(b: &mut Bencher) {
492 b.iter(|| tiles(32, 8, &[0.0], None));
493 }
494
495 #[bench]
496 fn bench_non_iht_tile_code_single_dimension(b: &mut Bencher) {
497 b.iter(|| tiles(2048, 8, &[0.0], None));
498 }
499
500 #[bench]
501 fn bench_non_iht_tile_code_small_four_dimensions(b: &mut Bencher) {
502 b.iter(|| tiles(32, 8, &[0.0, 1.0, 2.0, 3.0], None));
503 }
504
505 #[bench]
506 fn bench_non_iht_tile_code_four_dimensions(b: &mut Bencher) {
507 b.iter(|| tiles(2048, 8, &[0.0, 1.0, 2.0, 3.0], None));
508 }*/
509}