binding-swift/ExampleAgent.playground/Sources/GymClient.swift (177 lines of code) (raw):
//
// GymClient.swift
// GYM-HTTP-API
// Created by Andrew Schreiber on 2/2/17.
//
// Add this file to your iOS/MacOS project to access a Gym HTTP server
import Foundation
open class GymClient {
/// The URL for the Gym HTTP server
public let baseURL:URL
/// Creates an instance for interfacing with the Gym HTTP client.
public init(baseURL:URL) {
self.baseURL = baseURL
}
/// Create a new environment with a gym environment ID string i.e. "CartPole-v0"
/// - returns : An instanceID to be used to uniquely identify the environment to be manipulated
open func create(envID:EnvID) -> InstanceID {
let json = post(url: baseURL.appendingPathComponent("/v1/envs/"),
parameter: ["env_id":envID])
let instanceID = (json as! [String:InstanceID])["instance_id"]!
return instanceID
}
/// Get all existing environment instances. The list is reset each time the server is reset.
/// - returns : A dictionary of instanceIDs and the gym environment ID they are made from
open func listAll() -> [InstanceID:EnvID] {
let json = get(url: baseURL.appendingPathComponent("/v1/envs/"))
let map = (json as! [String:[InstanceID:EnvID]])["all_envs"]!
return map
}
/// Reset the state of the environment
/// The resulting observation type may vary.
/// For discrete spaces, it is an Int.
/// For vector spaces, it is a [Double].
/// - returns : An initial observation
open func reset(instanceID:InstanceID) -> Observation {
let json = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/reset/"))
let obs = (json as! [String:AnyObject])["observation"]!
return Observation(base: obs)
}
/// Run one timestep of the environment's dynamics.
/// - parameter action : An action to take. For discrete spaces, it should be an Int. For vector spaces, it should be a [Double].
/// - parameter render : Undocumented functionality XD
/// - returns : StepResult includes an observation of the current environment, amount of reward after the action, if the simulation is done, and any meta data
open func step(instanceID:InstanceID, action:Action, render:Bool = false) -> StepResult {
let parameter = ["action":action.base, "render":render] as [String : Any]
let json = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/step/"),
parameter: parameter)
let result = StepResult(jsonDict: json as! [String:AnyObject])
return result
}
/// Get information (name and dimensions/bounds) of the env's observation_space
open func observationSpace(instanceID:InstanceID) -> Space {
return getSpace(instanceID: instanceID, name: "observation_space")
}
/// Checks if observations are all contained in the observation space.
open func containsObservation(instanceID:InstanceID, observations:[String:Any]) -> Bool {
let json = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/observation_space/contains"), parameter: observations)
let isMember = (json as! [String:Bool])["member"]!
return isMember
}
/// Get information (name and dimensions/bounds) of the env's action_space
open func actionSpace(instanceID:InstanceID) -> Space {
return getSpace(instanceID: instanceID, name: "action_space")
}
/// Sample an action randomly from all possible actions in the environment
open func sampleAction(instanceID:InstanceID) -> Action {
let json = get(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/action_space/sample"))
let action = (json as! [String:AnyObject])["action"]!
return Action(base:action)
}
/// Checks if an action is contained in the action space. Currently, only int action types are supported
open func containsAction(instanceID:InstanceID, action:Action) -> Bool {
guard action.discreteValue != nil else { fatalError("Currently only int action types are supported") }
let json = get(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/action_space/contains/\(action.discreteValue!)"))
let isMember = (json as! [String:Bool])["member"]!
return isMember
}
/// Close the environment instance. Must be done before upload.
open func close(instanceID:InstanceID) {
_ = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/close/"))
}
/// Start recording.
/// - parameter directory : Location to write files. The server will create the directory if it does not exist.
/// - parameter force : Clear out existing training data from this directory (by deleting every file prefixed with "openaigym.")
/// - parameter resume : Retain the training data already in this directory, which will be merged with our new data
open func startMonitor(instanceID:InstanceID, directory:String, force:Bool, resume:Bool, videoCallable:Bool) {
let parameter = ["directory":directory,
"force": force,
"resume":resume,
"video_callable": videoCallable] as [String : Any]
_ = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/monitor/start/"), parameter: parameter)
}
/// Stop recording and flush all data to disk.
/// Two files will be created a meta data file like "openaigym.manifest.5.40273.manifest.json" and a performance file like "openaigym.episode_batch.11.40273.stats.json"
open func closeMonitor(instanceID:InstanceID) {
_ = post(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/monitor/close/"))
}
/// Shut down the server
open func shutdown() {
_ = post(url: baseURL.appendingPathComponent("/v1/shutdown/"))
}
/// Upload the results of training (as automatically recorded by your env's monitor) to OpenAI Gym.
/// - parameter directory : Absolute path of directory containing recorder files i.e. "/tmp/swift-gym-agent"
/// - parameter apiKey : Unique key from openai.com on your account page. Can be ignored if already contained in the environment.
/// - parameter algorithmID : A unique identifer for the algorithm. You can safely leave this nil and it will be autogenerated.
open func uploadResults(directory:String, apiKey:String?, algorithmID:String? = nil) {
guard let apiKey = apiKey ?? environmentVariable(key:"OPENAI_GYM_API_KEY") else { fatalError("No API Key") }
var data:[String:String] = ["training_dir": directory, "api_key": apiKey]
if let algorithmID = algorithmID {
data["algorithm_id"] = algorithmID
}
_ = post(url: baseURL.appendingPathComponent("/v1/upload/"), parameter: data)
}
// MARK: Helpers
private func get(url:URL) -> Any? {
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.timeoutInterval = 120
var json:Any?
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with: request) { (data, res, error) in
self.httpErrorHandler(data: data, res: res, error: error)
json = try! JSONSerialization.jsonObject(with: data!, options: [.allowFragments])
semaphore.signal()
}
task.resume()
_ = semaphore.wait(timeout: .distantFuture)
return json
}
private func post(url:URL, parameter:Any? = nil) -> Any? {
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 120
if let parameter = parameter {
request.httpBody = try! JSONSerialization.data(withJSONObject: parameter, options: [])
}
var json:Any?
let semaphore = DispatchSemaphore(value: 0)
let task = URLSession.shared.dataTask(with:request) { (data, res, error) in
self.httpErrorHandler(data: data, res: res, error: error)
json = try? JSONSerialization.jsonObject(with: data!, options: [.allowFragments])
semaphore.signal()
}
task.resume()
_ = semaphore.wait(timeout: .distantFuture)
return json
}
private func httpErrorHandler(data:Data?, res:URLResponse?, error:Error?) {
if let error = error {
fatalError(error.localizedDescription)
} else if let res = res as? HTTPURLResponse, ![200, 204].contains(res.statusCode) {
let text = String(data:data!, encoding:String.Encoding.utf8)!
fatalError("Error with request:\(text). Response: \(res)")
}
}
private func environmentVariable(key:String) -> String? {
guard let rawValue = getenv(key) else { return nil }
return String(utf8String: rawValue)
}
private func getSpace(instanceID:InstanceID, name:String) -> Space {
let json = get(url: baseURL.appendingPathComponent("/v1/envs/\(instanceID)/\(name)/"))
let dict = (json as! [String:AnyObject])["info"] as! [String:AnyObject]
return Space(jsonDict: dict)
}
}
// MARK: Models
public struct Space {
// Name is the name of the space, such as "Box", "HighLow",
// or "Discrete".
public let name:String
// Properties for Box spaces.
public let shape:[Int]?
public let low:[Double]?
public let high:[Double]?
// Properties for Discrete spaces.
public let n:Int?
// Properties for HighLow spaces.
public let numberOfRows:Int?
public let matrix:[Double]?
public init(jsonDict:[String:AnyObject]) {
name = jsonDict["name"] as! String
shape = jsonDict["shape"] as! [Int]?
low = jsonDict["low"] as! [Double]?
high = jsonDict["high"] as! [Double]?
n = jsonDict["n"] as! Int?
numberOfRows = jsonDict["num_rows"] as! Int?
matrix = jsonDict["matrix"] as! [Double]?
}
}
public typealias InstanceID = String
public typealias EnvID = String
public typealias Action = MultiValueType
public typealias Observation = MultiValueType
public struct MultiValueType {
public let base:AnyObject
public init(base:AnyObject) {
self.base = base
if vectorValue == nil && discreteValue == nil {
print("Unsupported value type: \(base)")
}
}
public var vectorValue:[Double]? {
return base as? [Double]
}
public var discreteValue:Int? {
return base as? Int
}
}
public struct StepResult {
public let observation:Observation
public let reward:Double
public let done:Bool
public let info:[String:AnyObject]
public init(jsonDict:[String:AnyObject]) {
self.observation = Observation(base: jsonDict["observation"]!)
self.reward = jsonDict["reward"] as! Double
self.done = jsonDict["done"] as! Bool
self.info = jsonDict["info"] as! [String:AnyObject]
}
}