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}