diff --git a/.gitignore b/.gitignore index 52510d6..77fc8e4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ *.ps static/* *.pyc +.idea \ No newline at end of file diff --git a/README.md b/README.md index 372b291..4c47326 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # About -MDPvis is a visualization designed to assist in the MDP simulation and optimization process. See the forthcoming research paper for more details, "Facilitating Testing and Debugging of Markov Decision Processes with Interactive Visualization." To play with a live version of the visualization, visit [mdpvis.github.io/](http://mdpvis.github.io/). +MDPvis is a visualization designed to assist in the MDP simulation and optimization process. See "[Facilitating Testing and Debugging of Markov Decision Processes with Interactive Visualization](http://ieeexplore.ieee.org/xpl/login.jsp?tp=&arnumber=7357198&url=http%3A%2F%2Fieeexplore.ieee.org%2Fxpls%2Fabs_all.jsp%3Farnumber%3D7357198)." To play with a live version of the visualization, visit [mdpvis.github.io/](http://mdpvis.github.io/). We built MDPvis as a web-based visualization so it would be: @@ -22,134 +22,120 @@ If you don't use our hosted version of the MDPvis web application, you will need 2. Clone MDPvis into your MDP simulator code base, `cd YOUR_SIMULATOR;git clone git@github.com:MDPvis/MDPvis.github.io.git`. You can use a [Git Submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules) for this if you don't mind figuring out how they work. If you are going to contribute back to MDPvis, you should probably clone your fork of MDPvis. 3. Install [Python 2.7](https://www.python.org/downloads/release/python-279/) 4. Install a few Python libraries with `pip install -U flask-cors`. -5. Navigate into the MDPvis directory and start the server with `python flask_server.py` - -At this point the server will start, but it will likely fail because the Flask server expects your MDP simulator to define a file in its code base called `domain_bridge.py`. The next (and final) section helps you define this file. +5. Navigate into the MDPvis directory and start the server with `./serve.sh` +6. Visit http://localhost:8000 +7. Select one of the pre-existing simulation and optimization servers, or bridge MDPvis to your domain # Bridging MDPvis and Your Domain -MDPvis interfaces with any MDP simulator+optimizer that is callable by a web server. If you use the web server packages with MDPvis, you can update an example `domain_bridge.py` file found in `example_domain_bridges`, otherwise we recommend viewing the example domain bridges and writing a version of the file as appropriate for your platform. +MDPvis interfaces with any MDP simulator+optimizer that is callable by a web server. -The bridges take the HTTP requests from the visualization, transforms the query parameters to those expected by the simulator or optimizer, invokes the simulator or optimizer, then returns the results to MDPvis. There are four distinct requests that the bridge should support. We detail these requests below +Your domain web server is responsible for serving four HTTP requests from the visualization, transforms the query parameters to those expected by the simulator or optimizer, invokes the simulator or optimizer, then returns the results to MDPvis. There are four distinct requests that the bridge should support. We detail these requests below. -The visualization expects your code to support the following requests. If your domain is written in Python, we recommend porting one of the example `domain_bridge.py` files to your domain. ## /initialize -The initialize endpoint doesn't query the simulator or optimizer, but it does provide a set of parameters that will influence those systems when requests are made. Here your responsibility is to return a [JSON](http://www.copterlabs.com/blog/json-what-it-is-how-it-works-how-to-use-it/) object listing the: +The `/initialize` endpoint provides a set of parameters that will be sent to the simulator or optimizer on future requests. Here your responsibility is to return a [JSON](http://www.copterlabs.com/blog/json-what-it-is-how-it-works-how-to-use-it/) object listing these properties: * Name * Description * Current Value * Minimum Value * Maximum Value +* Step (How fast the value changes when pressing a button) +* Units -of each parameter. An example of this data structure in Python is: +An example of this data structure in Python is: return { - "reward": [ - {"name": "Discount", - "description":"The per-year discount", - "current_value": 1, "max": 1, "min": 0, "units": "~"}, - {"name": "Suppression Variable Cost", - "description":"cost per hectare of suppression", - "current_value": 500, "max": 999999, "min": 0, "units": "$"} - ], - "transition": [ - {"name": "Years to simulate", - "description": "how far to look into the future", - "current_value": 10, "max": 150, "min": 0, "units": "Y"}, - {"name": "Harvest Percent", - "description": "timber harvest rate as a percent of annual increment", - "current_value": 0.95, "max": 1, "min": 0, "units": "%"}, - {"name": "Minimum Timber Value", - "description":"the minimum timber value required before harvest is allowed", - "current_value": 50, "max":9999, "min": 0, "units": "$"}, - {"name": "Growth Model", - "description": "set to 1 to use original model; or 2 for updated model.", - "current_value": 1, "max":2, "min": 1, "units": "~"} - ], - "policy": [ - {"name": "Constant", - "description":"for the intercept", - "current_value": 0, "max": 10, "min":-10, "units": ""}, - {"name": "Fuel Load 8", - "description":"for the average fuel load in the 8 neighboring stands", - "current_value": 0, "max": 10, "min":-10, "units": ""}, - {"name": "Fuel Load 24", - "description":"for the average fuel load in the 24 neighboring stands", - "current_value": 0, "max": 10, "min":-10, "units": ""} - ] - } - -In the MDPvis user interface, each control will be grouped into panels for the reward, model (transition function), and policy. - -## /rollouts?QUERY - -When requesting Monte Carlo rollouts, MDPvis will send the current set of parameters as defined in the initialization and assigned in the user interface. The job of the domain bridge is to map the parameters of the user interface into parameters to invoke the simulator. After simulations have completed, the data should be JSON serialized. An example of the data in Python is: - return [ + # The control panels that appear at the top of the screen + "parameter_collections": [ + { + "panel_title": "Sampling Effort", + "panel_icon": "glyphicon-retweet", + "panel_description": "Define how many trajectories you want to generate, and to what time horizon.", + "quantitative": [ # Real valued parameters + { + "name": "Sample Count", + "description": "Specify how many trajectories to generate", + "current_value": 10, + "max": 1000, + "min": 1, + "step": 10, + "units": "#" + }, + { + "name": "Horizon", + "description": "The time step at which simulation terminates", + "current_value": 10, + "max": 10000, + "min": 1, + "step": 10, + "units": "Time Steps" + }, + { + "name": "Seed", + "description": "The random seed used for simulations", + "current_value": 0, + "max": 100000, + "min": 1, + "step": 1, + "units": "NA" + } + ] + } + ] + } + +In the MDPvis user interface, each control will be grouped into panels under the `panel_title` +and display the [icon](http://getbootstrap.com/components/#glyphicons) specified by `panel_icon`. + +## /trajectories?QUERY + +When requesting Monte Carlo trajectories, MDPvis will send the current set of parameters as defined in the initialization and assigned in the user interface. The job of the web server is to map the parameters of the user interface into parameters to invoke the simulator. After simulations have completed, the data should be JSON serialized. An example of the data in Python is: + + return {"trajectories": [ [ {"Burn Time": 4.261, "Timber Harvested": 251}, {"Burn Time": 40.261, "Timber Harvested": 0} ], [ {"Burn Time": 0.0, "Timber Harvested": 342}, {"Burn Time": 45.261, "Timber Harvested": 20} ] - ] - -These data are two rollouts of two states each. - -## /optimize?QUERY - -MDPvis does not require you to integrate `/optimize` and `/state`, but it is very useful for exploring most problems. Here all the same parameters as are sent to `/rollouts` are sent to `/optimize`, but this query only returns an updated policy. Here is a python example: - - return [ - {"Constant": 10}, - {"Fuel Load 8": 3}, - {"Fuel Load 24": -1} - ] - -## /state?QUERY + ]} -This query will be issued when a user clicks an individual rollout in the visualization. All the parameters used to generate the rollout will be sent, with an additional parameter for the rollout number. The expectation is you will use this information to re-generate the rollout and use the simulator to generate descriptive statistics and/or images for the states. +These data are two trajectories of two states each. An additional special state variable, `image row`, gives +an array of images or videos that should be displayed when selecting a trajectory. For example: -An example of the expected return format is: - - return { - "images": [ - ["file_row1_column1.png", "file_row1_column2.png"], - ["file_row2_column1.png", "file_row2_column2.png"]] - "statistics": { - "stat 1": 5, - "stat 2": -100 - } - } - -# Adding Additional Visualizations - -There is a default set of visualizations, but if you want to add your own visualizations you should be aware of the three visualization types in MDPvis. These break Monte Carlo rollouts into visualizations for a single time step, variables through time (temporal distributions), and details on a single rollout. Details on these three aspects are below. - -**Single Time Step Distributions** + return {"trajectories": [ + [ + {"Burn Time": 4.261, "Timber Harvested": 251, "image row": ["traj1-1.png"]}, {"Burn Time": 40.261, "Timber Harvested": 0, , "image row": ["traj1-2.png"]} + ], + [ + {"Burn Time": 0.0, "Timber Harvested": 342, "image row": ["traj2-1.mp4"]}, {"Burn Time": 45.261, "Timber Harvested": 20, "image row": ["traj2-1.mp4"]} + ] + ]} -Monte Carlo rollouts produce a distribution of states at every time step. This view gives details on the distribution for the currently selected time step. Users may select the current time step for all these visualizations simultaneously from the top of the visualization area, or from the temporal distribution area. +will attempt to display the images `traj1-1.png` and `traj1-1.png` when the user clicks the associated trajectory. -* Histogram -* Bar Chart (comparison mode) +## /optimize?QUERY -**Temporal Distributions** +MDPvis does not require you to integrate `/optimize` and `/state`, but it is very useful for exploring most problems. Here all the same parameters as are sent to `/trajectories` are sent to `/optimize`, but this query only returns an updated policy. Here is a python example for a logistic regression based policy: -In this area we show how the distribution of state variables develops through time. + return {"Constant": 10, + "Fuel Load 8": 3, + "Fuel Load 24": -1} -* Fan Chart -* Fan Chart (comparison mode) -* Time Series +Here is an example where the policy parameters represent versions of a neural network. This would allow for comparing between the performances of different neural networks and +asking for additional training of an existing network. -**Single Rollout** + return [ + {"network version": 5} + ] -Here a single rollout is shown. This could give a sequence of state snapshots provided as images from the MDP simulator. +## /STATE_DETAIL -* State images (provided by simulator) -* Stats panel +This query will be issued when a user clicks an individual trajectory in the visualization and the state detail area populates with the images and videos specified by the trajectory's "image row" variable. The expectation is you will use the file name to re-generate the trajectory and use the simulator to generate descriptive statistics, videos, and/or images for the states. ## Implementing a New Visualization @@ -157,7 +143,7 @@ If you are interested in implementing a new visualization within MDPvis, we enco You've been warned. Here are the steps: -1. Select a visualization aspect (Single Time Step Distributions, Temporal Distributions, Single Rollout) +1. Select a visualization aspect (Single Time Step Distributions, Temporal Distributions, Single Trajectory) 1. Copy an existing visualization's script that has the chosen aspect 1. Add the script to index.html 1. Update the index.js script to call your visualization and add it to the DOM. @@ -172,4 +158,3 @@ Maintainer Mailing Address: PO Box 79, Corvallis, OR 97339, United States of Ame Implementation by: Sean McGregor With: Hailey Buckingham, Thomas G. Dietterich, Rachel Houtman, Claire Montgomery, and Ronald Metoyer - diff --git a/css/index.css b/css/index.css index 172fb9b..b16c476 100644 --- a/css/index.css +++ b/css/index.css @@ -70,6 +70,10 @@ body { display: none; } +.x.axis.show path { + display: inherit; +} + .fire_histogram_axis_label { font-size: large; } @@ -218,10 +222,11 @@ body { .line { fill: none; stroke-width: 2px; + opacity: 0.3; } .state-detail { - cursor: help; + cursor: pointer; } .statistics-area { @@ -273,3 +278,108 @@ body { .lighten { opacity: 0.5; } + +.counts { + font-weight: bold; + font-size: 1.4em; +} + +.affix-panel { + padding: 0px 5px 5px 0px; +} + +.text-right { + margin-right: 8px; +} + +.margin { + margin: 8px; +} + +.show-chart-button, .context-panel-button { + margin: 2px; +} + +.affix-top { + min-height: 60px; +} + +.temporal_zoom { + opacity: 0; +} + +.temporal_zoom:hover { + opacity: 0.1; + color: #222222; + cursor: zoom-in; +} + +.temporal-brush-boundary { + cursor:pointer; +} + +.hover_line:hover { + stroke: yellow; + stroke-width: 8px; + cursor: pointer; +} + +.axis text { + text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff; + cursor: move; +} + +.color-cycle, .color-cycle.selected_line { + animation-name: selectedCycle; + animation-duration:6s; + animation-iteration-count:infinite; + + -webkit-animation-name: selectedCycle; + -webkit-animation-duration:3s; + -webkit-animation-iteration-count:infinite; } + +@keyframes selectedCycle +{ + 0% {stroke:green;} + 25% {stroke:red;} + 50% {stroke:green;} + 75% {stroke:red;} +} + +@-webkit-keyframes selectedCycle +{ + 0% {stroke:green;} + 25% {stroke:red;} + 50% {stroke:green;} + 75% {stroke:red;} +} + +.parameter-line { + stroke: grey; + fill: none; + stroke-width: 2px; +} + +.selected_line { + stroke-width: 4px; + stroke-dasharray: 5,10,5; + stroke: red; +} + +.viewed-parameter-line { + stroke: green; + stroke-width: 4px; +} + +.compared-parameter-line { + stroke: red; + stroke-width: 4px; +} + +.btn { + min-width: 220px; +} + +.modal-title { + text-align: center; +} \ No newline at end of file diff --git a/flask_server.py b/flask_server.py index 1bda0dd..3fe7f2c 100644 --- a/flask_server.py +++ b/flask_server.py @@ -11,6 +11,20 @@ from flask import Flask, jsonify, request, redirect import sys +print """ +Starting Flask Server... +Note, you may be able to specify a domain at this point by adding it as a +positional argument. +""" + +# Specify which domain should be selected from the domain bridge. +# This is an optional argument that is used for domain_bridges that sit +# atop a collection of MDP domains. You should leave this as the +# empty string unless you must select between domains. +domain = "" +if len(sys.argv) > 1: + domain = sys.argv[1] + # Add the parent folder path to the sys.path list so # we can include its bridge sys.path.insert(0, '..') @@ -69,7 +83,7 @@ def site_root(): integration with your MDP domain and optimizer.
To test the other endpoints, visit /initialize, - /rollouts, + /trajectories, /optimize, or /state ''' @@ -82,16 +96,16 @@ def cross_origin_initialize(): ''' return jsonify(domain_bridge.initialize()) -@app.route("/rollouts", methods=['GET']) +@app.route("/trajectories", methods=['GET']) @cross_origin(allow_headers=['Content-Type']) -def cross_origin_rollouts(): +def cross_origin_trajectories(): ''' - Asks the domain for the rollouts generated by the + Asks the domain for the trajectories generated by the requested parameters. ''' q = parse_query(request.args) - rollouts = domain_bridge.rollouts(q) - return jsonify({"rollouts": rollouts}) + trajectories = domain_bridge.trajectories(q) + return jsonify({"trajectories": trajectories}) @app.route("/optimize", methods=['GET']) @cross_origin(allow_headers=['Content-Type']) diff --git a/index.html b/index.html index e3a3481..7872aef 100644 --- a/index.html +++ b/index.html @@ -32,13 +32,14 @@