diff --git a/README.md b/README.md index 6999d4b..f88d0f1 100644 --- a/README.md +++ b/README.md @@ -140,4 +140,4 @@ API specification Contributors ============ -See the [contributors page] (https://github.com/openai/gym-http-api/graphs/contributors) +See the [contributors page](https://github.com/openai/gym-http-api/graphs/contributors) diff --git a/binding-rust/Cargo.toml b/binding-rust/Cargo.toml index a83031c..16bbcbd 100644 --- a/binding-rust/Cargo.toml +++ b/binding-rust/Cargo.toml @@ -5,5 +5,6 @@ authors = ["NivenT "] [dependencies] rand = "0.3.14" -serde_json = "0.8.0" -hyper = "0.9.12" +serde = "1.0.10" +serde_json = "1.0.2" +reqwest = "0.7.1" \ No newline at end of file diff --git a/binding-rust/examples/random_agent.rs b/binding-rust/examples/random_agent.rs index a92492c..f6703ac 100644 --- a/binding-rust/examples/random_agent.rs +++ b/binding-rust/examples/random_agent.rs @@ -1,20 +1,20 @@ extern crate gym; -use gym::*; +use gym::GymClient; fn main() { println!("**********************************"); - let mut client = GymClient::new("http://localhost:5000".to_string()); - //println!("already running environments:\n{:?}\n", client.get_envs().unwrap()); + let client = GymClient::new("http://localhost:5000".to_string()).unwrap(); + println!("already running environments:\n{:?}\n", client.get_envs().unwrap()); - let mut env = match client.make("CartPole-v0") { + let env = match client.make("CartPole-v0") { Ok(env) => env, Err(msg) => panic!("Could not make environment because of error:\n{}", msg) }; - //println!("observation space:\n{:?}\n", env.observation_space()); - //println!("action space:\n{:?}\n", env.action_space()); + println!("observation space:\n{:?}\n", env.observation_space()); + println!("action space:\n{:?}\n", env.action_space()); let _ = env.monitor_start("/tmp/random-agent-results".to_string(), true, false); for ep in 0..10 { diff --git a/binding-rust/src/error.rs b/binding-rust/src/error.rs new file mode 100644 index 0000000..ff7877b --- /dev/null +++ b/binding-rust/src/error.rs @@ -0,0 +1,44 @@ +use std::error::Error; +use std::fmt; + +use serde_json; + +use reqwest; + +pub type GymResult = Result; + +#[derive(Debug)] +pub enum GymError { + Connection(reqwest::Error), + Json(serde_json::Error) +} + +impl fmt::Display for GymError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + GymError::Connection(ref err) => fmt::Display::fmt(err, f), + GymError::Json(ref err) => fmt::Display::fmt(err, f), + } + } +} + +impl Error for GymError { + fn description(&self) -> &str { + match *self { + GymError::Connection(ref err) => err.description(), + GymError::Json(ref err) => err.description() + } + } +} + +impl From for GymError { + fn from(err: reqwest::Error) -> GymError { + GymError::Connection(err) + } +} + +impl From for GymError { + fn from(err: serde_json::Error) -> GymError { + GymError::Json(err) + } +} \ No newline at end of file diff --git a/binding-rust/src/lib.rs b/binding-rust/src/lib.rs index eb2d54a..792bbc9 100644 --- a/binding-rust/src/lib.rs +++ b/binding-rust/src/lib.rs @@ -1,226 +1,149 @@ +extern crate serde; extern crate serde_json; -extern crate hyper; + +extern crate reqwest; + extern crate rand; +mod error; +mod space; + use std::collections::BTreeMap; -use std::io::Read; +use serde::ser::Serialize; use serde_json::Value; -use serde_json::value::{ToJson, from_value}; +use serde_json::value::from_value; -use hyper::client::Client; -use hyper::header::Headers; +use reqwest::Client; +use reqwest::header::ContentType; -use rand::{thread_rng, Rng}; - -pub type GymResult = Result; - -#[derive(Debug, Clone)] -pub enum Space { - DISCRETE{n: u64}, - BOX{shape: Vec, high: Vec, low: Vec}, - TUPLE{spaces: Vec>} -} +pub use self::error::{GymResult, GymError}; +pub use self::space::Space; -impl Space { - fn from_json(info: &Value) -> Space { - match info.find("name").unwrap().as_str().unwrap() { - "Discrete" => { - let n = info.find("n").unwrap().as_u64().unwrap(); - Space::DISCRETE{n: n} - }, - "Box" => { - let shape = info.find("shape").unwrap().as_array().unwrap() - .into_iter().map(|x| x.as_u64().unwrap()) - .collect::>(); - - - let high = info.find("high").unwrap().as_array().unwrap() - .into_iter().map(|x| x.as_f64().unwrap()) - .collect::>(); - - let low = info.find("low").unwrap().as_array().unwrap() - .into_iter().map(|x| x.as_f64().unwrap()) - .collect::>(); - - Space::BOX{shape: shape, high: high, low: low} - }, - "Tuple" => panic!("Parsing for Tuple spaces is not yet implemented"), - e @ _ => panic!("Unrecognized space name: {}", e) - } - } - pub fn sample(&self) -> Vec { - let mut rng = thread_rng(); - match *self { - Space::DISCRETE{n} => { - vec![(rng.gen::()%n) as f64] - }, - Space::BOX{ref shape, ref high, ref low} => { - let mut ret = Vec::with_capacity(shape.iter().map(|x| *x as usize).product()); - let mut index = 0; - for &i in shape { - for _ in 0..i { - ret.push(rng.gen_range(low[index], high[index])); - index += 1; - } - } - ret - }, - Space::TUPLE{ref spaces} => { - let mut ret = Vec::new(); - for space in spaces { - ret.extend(space.sample()); - } - ret - } - } - } -} - -#[allow(dead_code)] #[derive(Debug)] pub struct State { - pub observation: Vec, - pub reward: f64, - pub done: bool, - pub info: Value, + pub observation: Vec, + pub reward: f64, + pub done: bool, + pub info: Value, } -#[allow(dead_code)] pub struct Environment { - client: GymClient, - instance_id: String, - act_space: Space, - obs_space: Space, + client: GymClient, + instance_id: String, + act_space: Space, + obs_space: Space, } impl Environment { - pub fn action_space<'a>(&'a self) -> &'a Space { - &self.act_space - } - pub fn observation_space<'a>(&'a self) -> &'a Space { - &self.obs_space - } - pub fn reset(&mut self) -> GymResult> { - let path = "/v1/envs/".to_string() + &self.instance_id + "/reset/"; - let observation = try!(self.client.post(path, Value::Null)); - - let ret: Vec<_> = observation.find("observation").unwrap().as_array().unwrap() - .into_iter().map(|x| x.as_f64().unwrap()) - .collect(); - Ok(ret) - } - pub fn step(&mut self, action: Vec, render: bool) -> GymResult { - let mut req = BTreeMap::new(); - req.insert("render", Value::Bool(render)); - match self.act_space { - Space::DISCRETE{..} => { - assert_eq!(action.len(), 1); - req.insert("action", Value::U64(action[0] as u64)); - }, - Space::BOX{ref shape, ..} => { - assert_eq!(action.len(), shape[0] as usize); - req.insert("action", action.to_json()); - }, - Space::TUPLE{..} => panic!("Actions for Tuple spaces not implemented yet") - } - - let path = "/v1/envs/".to_string() + &self.instance_id + "/step/"; - let state = try!(self.client.post(path, req.to_json())); - - Ok(State { - observation: from_value(state.find("observation").unwrap().clone()).unwrap(), - reward: state.find("reward").unwrap().as_f64().unwrap(), - done: state.find("done").unwrap().as_bool().unwrap(), - info: state.find("info").unwrap().clone() - }) - } - pub fn monitor_start(&mut self, directory: String, force: bool, resume: bool) -> GymResult<()> { - let mut req = BTreeMap::new(); - req.insert("directory", Value::String(directory)); - req.insert("force", Value::Bool(force)); - req.insert("resume", Value::Bool(resume)); - - let path = "/v1/envs/".to_string() + &self.instance_id + "/monitor/start/"; - try!(self.client.post(path, req.to_json())); - Ok(()) - } - pub fn monitor_stop(&mut self) -> GymResult<()> { - let path = "/v1/envs/".to_string() + &self.instance_id + "/monitor/close/"; - try!(self.client.post(path, Value::Null)); - Ok(()) - } - pub fn upload(&mut self, training_dir: String, api_key: String, algorithm_id: String) -> GymResult<()> { - let mut req = BTreeMap::new(); - req.insert("training_dir", training_dir); - req.insert("api_key", api_key); - req.insert("algorithm_id", algorithm_id); - - try!(self.client.post("/v1/upload/".to_string(), req.to_json())); - Ok(()) - } + pub fn action_space<'a>(&'a self) -> &'a Space { + &self.act_space + } + pub fn observation_space<'a>(&'a self) -> &'a Space { + &self.obs_space + } + pub fn reset(&self) -> GymResult> { + let path = "/v1/envs/".to_string() + &self.instance_id + "/reset/"; + let observation = self.client.post(path, &Value::Null)?; + + Ok(from_value(observation["observation"].clone()) + .expect("Should only panic if the API changes")) + } + pub fn step(&self, action: Vec, render: bool) -> GymResult { + let mut req = BTreeMap::new(); + req.insert("render", Value::Bool(render)); + match self.act_space { + Space::DISCRETE{..} => { + debug_assert_eq!(action.len(), 1); + req.insert("action", (action[0] as u64).into()); + }, + Space::BOX{ref shape, ..} => { + debug_assert_eq!(action.len(), shape.iter().map(|&x| x as usize).product::()); + req.insert("action", action.into()); + }, + Space::TUPLE{..} => panic!("Actions for Tuple spaces not implemented yet") + } + + let path = "/v1/envs/".to_string() + &self.instance_id + "/step/"; + let state = self.client.post(path, &req)?; + + Ok(State { + observation: from_value(state["observation"].clone()).unwrap(), + reward: state["reward"].as_f64().unwrap(), + done: state["done"].as_bool().unwrap(), + info: state["info"].clone() + }) + } + pub fn monitor_start(&self, directory: String, force: bool, resume: bool) -> GymResult { + let mut req = BTreeMap::new(); + req.insert("directory", Value::String(directory)); + req.insert("force", Value::Bool(force)); + req.insert("resume", Value::Bool(resume)); + + let path = "/v1/envs/".to_string() + &self.instance_id + "/monitor/start/"; + self.client.post(path, &req) + } + pub fn monitor_stop(&self) -> GymResult { + let path = "/v1/envs/".to_string() + &self.instance_id + "/monitor/close/"; + self.client.post(path, &Value::Null) + } + pub fn upload(&self, training_dir: String, api_key: String, algorithm_id: String) -> GymResult { + let mut req = BTreeMap::new(); + req.insert("training_dir", training_dir); + req.insert("api_key", api_key); + req.insert("algorithm_id", algorithm_id); + + self.client.post("/v1/upload/".to_string(), &req) + } } pub struct GymClient { - address: String, - handle: Client, - headers: Headers, + address: String, + handle: Client, } impl GymClient { - pub fn new(addr: String) -> GymClient { - let mut headers = Headers::new(); - headers.set_raw("Content-Type", vec![b"application/json".to_vec()]); - - GymClient { - address: addr, - handle: Client::new(), - headers: headers - } + pub fn new(addr: String) -> GymResult { + Ok(GymClient { + address: addr, + handle: Client::new()?, + }) } - pub fn make(mut self, env_id: &str) -> GymResult { - let mut req: BTreeMap<&str, &str> = BTreeMap::new(); - req.insert("env_id", env_id); - - let instance_id = try!(self.post("/v1/envs/".to_string(), req.to_json())); - let instance_id = match instance_id.find("instance_id") { - Some(id) => id.as_str().unwrap(), - None => panic!("Unrecognized environment id: {}", env_id) - }; - - let obs_space = try!(self.get("/v1/envs/".to_string() + instance_id + "/observation_space/")); - let act_space = try!(self.get("/v1/envs/".to_string() + instance_id + "/action_space/")); - - Ok(Environment { - client: self, - instance_id: instance_id.to_string(), - act_space: Space::from_json(act_space.find("info").unwrap()), - obs_space: Space::from_json(obs_space.find("info").unwrap())}) + pub fn make(self, env_id: &str) -> GymResult { + let mut req: BTreeMap<&str, &str> = BTreeMap::new(); + req.insert("env_id", env_id); + + let instance_id = self.post("/v1/envs/".to_string(), &req)?; + let instance_id = instance_id["instance_id"].as_str().unwrap(); + + let obs_space = self.get("/v1/envs/".to_string() + instance_id + "/observation_space/")?; + let act_space = self.get("/v1/envs/".to_string() + instance_id + "/action_space/")?; + + Ok(Environment { + client: self, + instance_id: instance_id.to_string(), + act_space: Space::from_json(&act_space["info"])?, + obs_space: Space::from_json(&obs_space["info"])?}) } - pub fn get_envs(&mut self) -> GymResult> { - let json = try!(self.get("/v1/envs/".to_string())); - Ok(from_value(json.find("all_envs").unwrap().clone()).unwrap()) + pub fn get_envs(&self) -> GymResult> { + let json = self.get("/v1/envs/".to_string())?; + Ok(from_value(json["all_envs"].clone()).unwrap()) } - fn post(&mut self, route: String, request: Value) -> GymResult { - let url = self.address.clone() + &route; - let mut resp = try!(self.handle.post(&url) - .body(&request.to_string()) - .headers(self.headers.clone()) - .send()); - - let mut json = String::new(); - let _ = resp.read_to_string(&mut json); - Ok(serde_json::from_str(&json).unwrap_or(Value::Null)) + fn post(&self, route: String, request: &T) -> GymResult { + let url = self.address.clone() + &route; + match self.handle.post(&url)?.header(ContentType::json()).json(request)?.send()?.json() { + Ok(val) => Ok(val), + Err(e) => Err(e.into()), + } + } - fn get(&mut self, route: String) -> GymResult { - let url = self.address.clone() + &route; - let mut resp = try!(self.handle.get(&url) - .send()); - let mut json = String::new(); - let _ = resp.read_to_string(&mut json); - - Ok(serde_json::from_str(&json).unwrap_or(Value::Null)) + fn get(&self, route: String) -> GymResult { + let url = self.address.clone() + &route; + match self.handle.get(&url)?.send()?.json() { + Ok(val) => Ok(val), + Err(e) => Err(e.into()), + } } } \ No newline at end of file diff --git a/binding-rust/src/space.rs b/binding-rust/src/space.rs new file mode 100644 index 0000000..0850089 --- /dev/null +++ b/binding-rust/src/space.rs @@ -0,0 +1,76 @@ +use std::iter; + +use serde_json::Value; +use serde_json::value::from_value; + +use error::GymResult; + +use rand::{thread_rng, Rng}; + +fn tuples(sizes: &[u64]) -> Vec> { + match sizes.len() { + 0 => vec![], + 1 => (0..sizes[0]).map(|x| vec![x]).collect(), + _ => { + let (&head, tail) = sizes.split_first().unwrap(); + (0..head).flat_map(|x| iter::repeat(x).zip(tuples(tail)) + .map(|(h, mut t)| { + t.insert(0, h); + t + }) + ).collect() + } + } +} + +#[derive(Debug, Clone)] +pub enum Space { + DISCRETE{n: u64}, + BOX{shape: Vec, high: Vec, low: Vec}, + TUPLE{spaces: Vec>} +} + +impl Space { + pub(crate) fn from_json(info: &Value) -> GymResult { + match info["name"].as_str().unwrap() { + "Discrete" => { + let n = info["n"].as_u64().unwrap(); + Ok(Space::DISCRETE{n: n}) + }, + "Box" => { + let shape = from_value(info["shape"].clone())?; + let high = from_value(info["high"].clone())?; + let low = from_value(info["low"].clone())?; + + Ok(Space::BOX{shape: shape, high: high, low: low}) + }, + "Tuple" => panic!("Parsing for Tuple spaces is not yet implemented"), + e @ _ => panic!("Unrecognized space name: {}", e) + } + } + pub fn sample(&self) -> Vec { + let mut rng = thread_rng(); + match *self { + Space::DISCRETE{n} => { + vec![(rng.gen::()%n) as f64] + }, + Space::BOX{ref shape, ref high, ref low} => { + let mut ret = Vec::with_capacity(shape.iter().map(|x| *x as usize).product()); + let mut index = 0; + + for _ in tuples(shape) { + ret.push(rng.gen_range(low[index], high[index])); + index += 1 + } + ret + }, + Space::TUPLE{ref spaces} => { + let mut ret = Vec::new(); + for space in spaces { + ret.extend(space.sample()); + } + ret + } + } + } +} \ No newline at end of file diff --git a/binding-rust/tests/lib.rs b/binding-rust/tests/lib.rs index 569dd6f..7afab47 100644 --- a/binding-rust/tests/lib.rs +++ b/binding-rust/tests/lib.rs @@ -2,20 +2,25 @@ extern crate gym; use gym::*; +const NUM_SAMPLES: usize = 25; + #[test] -fn test_space_sample() { +fn test_discrete_space_sample() { let discrete_space = Space::DISCRETE{n: 15}; - for _ in 0..10 { + for _ in 0..NUM_SAMPLES { let sample = discrete_space.sample(); assert!(sample.len() == 1 && 0. <= sample[0] && sample[0] < 15.); } +} +#[test] +fn test_box_space_sample() { let box_space = Space::BOX{ shape: vec![5], high: vec![1., 2., 3., 4., 5.], low: vec![-1., -2., -3., -4., -5.] }; - for _ in 0..10 { + for _ in 0..NUM_SAMPLES { let sample = box_space.sample(); assert_eq!(sample.len(), 5); for i in 0..5 { @@ -23,9 +28,41 @@ fn test_space_sample() { assert!(-bound <= sample[i] && sample[i] <= bound); } } +} + +#[test] +fn test_big_box_space_sample() { + let box_space = Space::BOX { + shape: vec![3, 2, 2], + high: vec![1., 2., 3., 4., 5., 6., 7., 8., 9., 10., 11., 12.], + low: vec![-1., -2., -3., -4., -5., -6., -7., -8., -9., -10., -11., -12.] + }; + for _ in 0..NUM_SAMPLES { + let sample = box_space.sample(); + assert_eq!(sample.len(), 12); + for i in 0..12 { + let bound = (i+1) as f64; + assert!(-bound <= sample[i] && sample[i] <= bound); + } + } +} - let tuple_space = Space::TUPLE{spaces: vec![Box::new(discrete_space), Box::new(box_space)]}; - for _ in 0..10 { +#[test] +fn test_tuple_space_sample() { + let discrete_space = Space::DISCRETE{n: 15}; + let box_space = Space::BOX { + shape: vec![5], + high: vec![1., 2., 3., 4., 5.], + low: vec![-1., -2., -3., -4., -5.] + }; + + let tuple_space = Space::TUPLE { + spaces: vec![ + Box::new(discrete_space), + Box::new(box_space) + ] + }; + for _ in 0..NUM_SAMPLES { let sample = tuple_space.sample(); assert_eq!(sample.len(), 6); assert!(0. <= sample[0] && sample[0] <= 15.);