diff --git a/.gitignore b/.gitignore
index b899f89b51f1efee5dd6faae0b675a525813d55a..ea4170641b8aa2fd92f7476510eba1b36944a824 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,4 @@
 tags
 test
 models
-terraform.tfstate
-terraform.tfstate.backup
-terraform.tfvars
 .idea
diff --git a/README.md b/README.md
index 4ca23bf632bc54c90f3e04bf573fbd8c88b98579..c7282686da9a66e55ac23927785115af6413103b 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@ Based on [pix2pix](https://phillipi.github.io/pix2pix/) by Isola et al.
 
 [Article about this implemention](https://affinelayer.com/pix2pix/)
 
+[Interactive Demo](https://affinelayer.com/pixsrv/)
+
 Tensorflow implementation of pix2pix.  Learns a mapping from input images to output images, like these examples from the original paper:
 
 <img src="docs/examples.jpg" width="900px"/>
@@ -13,7 +15,7 @@ This port is based directly on the torch implementation, and not on an existing
 ## Setup
 
 ### Prerequisites
-- Tensorflow 1.0.0
+- Tensorflow 1.4.1
 
 ### Recommended
 - Linux with Tensorflow GPU edition + cuDNN
@@ -71,15 +73,15 @@ For example:
 
 <img src="docs/418.png" width="256px"/>
 
-Some datasets have been made available by the authors of the pix2pix paper.  To download those datasets, use the included script `tools/download-dataset.py`.  There are also links to pre-trained models alongside each dataset, note that these pre-trained models require the Tensorflow 0.12.1 version of pix2pix.py since they have not been regenerated with the 1.0.0 release:
+Some datasets have been made available by the authors of the pix2pix paper.  To download those datasets, use the included script `tools/download-dataset.py`.  There are also links to pre-trained models alongside each dataset, note that these pre-trained models require the current version of pix2pix.py:
 
 | dataset | example |
 | --- | --- |
-| `python tools/download-dataset.py facades` <br> 400 images from [CMP Facades dataset](http://cmp.felk.cvut.cz/~tylecr1/facade/). (31MB) <br> Pre-trained: [BtoA](https://mega.nz/#!2xpyQBoK!GVtkZN7lqY4aaZltMFdZsPNVE6bUsWyiVUN6RwJtIxQ)  | <img src="docs/facades.jpg" width="256px"/> |
-| `python tools/download-dataset.py cityscapes` <br> 2975 images from the [Cityscapes training set](https://www.cityscapes-dataset.com/). (113M) <br> Pre-trained: [AtoB](https://mega.nz/#!rxByxK6S!W9ZBUqgdGTFDWVlOE_ljVt1G3bU89bdu_nS9Bi1ujiA) [BtoA](https://mega.nz/#!b1olDbhL!mxsYC5AF_WH64CXoukN0KB-nw15kLQ0Etii-F-HDTps) | <img src="docs/cityscapes.jpg" width="256px"/> |
-| `python tools/download-dataset.py maps` <br> 1096 training images scraped from Google Maps (246M) <br> Pre-trained: [AtoB](https://mega.nz/#!i8pkkBJT!3NKLar9sUr-Vh_vNVQF-xwK9-D9iCqaCmj1T27xRf4w) [BtoA](https://mega.nz/#!r8xwCBCD!lNBrY_2QO6pyUJziGj7ikPheUL_yXA8xGXFlM3GPL3c) | <img src="docs/maps.jpg" width="256px"/> |
-| `python tools/download-dataset.py edges2shoes` <br> 50k training images from [UT Zappos50K dataset](http://vision.cs.utexas.edu/projects/finegrained/utzap50k/). Edges are computed by [HED](https://github.com/s9xie/hed) edge detector + post-processing. (2.2GB) <br> Pre-trained: [AtoB](https://mega.nz/#!OoYT3QiQ!8y3zLESvhOyeA6UsjEbcJphi3_uEt534waSL5_f_D4Y) | <img src="docs/edges2shoes.jpg" width="256px"/>  |
-| `python tools/download-dataset.py edges2handbags` <br> 137K Amazon Handbag images from [iGAN project](https://github.com/junyanz/iGAN). Edges are computed by [HED](https://github.com/s9xie/hed) edge detector + post-processing. (8.6GB) <br> Pre-trained: [AtoB](https://mega.nz/#!KlpBHKrZ!iJ3x6xzgk0wnJkPiAf0UxPzhYSmpC3kKH1DY5n_dd0M) | <img src="docs/edges2handbags.jpg" width="256px"/> |
+| `python tools/download-dataset.py facades` <br> 400 images from [CMP Facades dataset](http://cmp.felk.cvut.cz/~tylecr1/facade/). (31MB) <br> Pre-trained: [BtoA]()  | <img src="docs/facades.jpg" width="256px"/> |
+| `python tools/download-dataset.py cityscapes` <br> 2975 images from the [Cityscapes training set](https://www.cityscapes-dataset.com/). (113M) <br> Pre-trained: [AtoB]() [BtoA]() | <img src="docs/cityscapes.jpg" width="256px"/> |
+| `python tools/download-dataset.py maps` <br> 1096 training images scraped from Google Maps (246M) <br> Pre-trained: [AtoB]() [BtoA]() | <img src="docs/maps.jpg" width="256px"/> |
+| `python tools/download-dataset.py edges2shoes` <br> 50k training images from [UT Zappos50K dataset](http://vision.cs.utexas.edu/projects/finegrained/utzap50k/). Edges are computed by [HED](https://github.com/s9xie/hed) edge detector + post-processing. (2.2GB) <br> Pre-trained: [AtoB]() | <img src="docs/edges2shoes.jpg" width="256px"/>  |
+| `python tools/download-dataset.py edges2handbags` <br> 137K Amazon Handbag images from [iGAN project](https://github.com/junyanz/iGAN). Edges are computed by [HED](https://github.com/s9xie/hed) edge detector + post-processing. (8.6GB) <br> Pre-trained: [AtoB]() | <img src="docs/edges2handbags.jpg" width="256px"/> |
 
 The `facades` dataset is the smallest and easiest to get started with.
 
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 8d1ab044e695f732ce4ffa29354be88541dcd173..2a0bc78c946fc808fe04329af8175d797bd1c26f 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,4 +1,4 @@
-FROM nvidia/cuda:8.0-cudnn5-devel-ubuntu16.04
+FROM nvidia/cuda:8.0-cudnn6-devel-ubuntu16.04
 
 WORKDIR /root
 
@@ -11,6 +11,7 @@ RUN apt-get install -y --no-install-recommends \
         cmake \
         git \
         wget \
+        curl \
         libatlas-base-dev \
         libboost-all-dev \
         libgflags-dev \
@@ -80,7 +81,6 @@ RUN curl -O https://raw.githubusercontent.com/pdollar/edges/master/private/edges
 # from https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/docker/Dockerfile.gpu
 RUN apt-get install -y --no-install-recommends \
         build-essential \
-        curl \
         libfreetype6-dev \
         libpng12-dev \
         libzmq3-dev \
@@ -116,8 +116,4 @@ RUN pip install \
     rsa==3.4.2 \
     six==1.10.0 \
     uritemplate==3.0.0 \
-    tensorflow-gpu==1.0.0
-
-RUN curl -O https://releases.hashicorp.com/terraform/0.8.7/terraform_0.8.7_linux_amd64.zip && \
-    unzip terraform_0.8.7_linux_amd64.zip -d /usr/local/bin && \
-    rm terraform_0.8.7_linux_amd64.zip
\ No newline at end of file
+    tensorflow-gpu==1.4.1
\ No newline at end of file
diff --git a/pix2pix.py b/pix2pix.py
index 20ba819340c414b14ba753047919744e14a777f4..291543fa5a08e44d8b223b31b488ef387f9a7098 100644
--- a/pix2pix.py
+++ b/pix2pix.py
@@ -91,14 +91,9 @@ def augment(image, brightness):
 
 
 def conv(batch_input, out_channels, stride):
-    with tf.variable_scope("conv"):
-        in_channels = batch_input.get_shape()[3]
-        filter = tf.get_variable("filter", [4, 4, in_channels, out_channels], dtype=tf.float32, initializer=tf.random_normal_initializer(0, 0.02))
-        # [batch, in_height, in_width, in_channels], [filter_width, filter_height, in_channels, out_channels]
-        #     => [batch, out_height, out_width, out_channels]
-        padded_input = tf.pad(batch_input, [[0, 0], [1, 1], [1, 1], [0, 0]], mode="CONSTANT")
-        conv = tf.nn.conv2d(padded_input, filter, [1, stride, stride, 1], padding="VALID")
-        return conv
+    # [batch, in_height, in_width, in_channels] => [batch, out_height, out_width, out_channels]
+    padded_input = tf.pad(batch_input, [[0, 0], [1, 1], [1, 1], [0, 0]], mode="CONSTANT")
+    return tf.layers.conv2d(padded_input, out_channels, kernel_size=4, strides=(stride, stride), padding="valid", kernel_initializer=tf.random_normal_initializer(0, 0.02))
 
 
 def lrelu(x, a):
@@ -113,28 +108,13 @@ def lrelu(x, a):
         return (0.5 * (1 + a)) * x + (0.5 * (1 - a)) * tf.abs(x)
 
 
-def batchnorm(input):
-    with tf.variable_scope("batchnorm"):
-        # this block looks like it has 3 inputs on the graph unless we do this
-        input = tf.identity(input)
-
-        channels = input.get_shape()[3]
-        offset = tf.get_variable("offset", [channels], dtype=tf.float32, initializer=tf.zeros_initializer())
-        scale = tf.get_variable("scale", [channels], dtype=tf.float32, initializer=tf.random_normal_initializer(1.0, 0.02))
-        mean, variance = tf.nn.moments(input, axes=[0, 1, 2], keep_dims=False)
-        variance_epsilon = 1e-5
-        normalized = tf.nn.batch_normalization(input, mean, variance, offset, scale, variance_epsilon=variance_epsilon)
-        return normalized
+def batchnorm(inputs):
+    return tf.layers.batch_normalization(inputs, axis=3, epsilon=1e-5, momentum=0.1, training=True, gamma_initializer=tf.random_normal_initializer(1.0, 0.02))
 
 
 def deconv(batch_input, out_channels):
-    with tf.variable_scope("deconv"):
-        batch, in_height, in_width, in_channels = [int(d) for d in batch_input.get_shape()]
-        filter = tf.get_variable("filter", [4, 4, out_channels, in_channels], dtype=tf.float32, initializer=tf.random_normal_initializer(0, 0.02))
-        # [batch, in_height, in_width, in_channels], [filter_width, filter_height, out_channels, in_channels]
-        #     => [batch, out_height, out_width, out_channels]
-        conv = tf.nn.conv2d_transpose(batch_input, filter, [batch, in_height * 2, in_width * 2, out_channels], [1, 2, 2, 1], padding="SAME")
-        return conv
+    # [batch, in_height, in_width, in_channels] => [batch, out_height, out_width, out_channels]
+    return tf.layers.conv2d_transpose(batch_input, out_channels, kernel_size=4, strides=(2, 2), padding="same", kernel_initializer=tf.random_normal_initializer(0, 0.02))
 
 
 def check_image(image):
@@ -430,7 +410,7 @@ def create_model(inputs, targets):
 
         return layers[-1]
 
-    with tf.variable_scope("generator") as scope:
+    with tf.variable_scope("generator"):
         out_channels = int(targets.get_shape()[-1])
         outputs = create_generator(inputs, out_channels)
 
@@ -475,7 +455,7 @@ def create_model(inputs, targets):
     ema = tf.train.ExponentialMovingAverage(decay=0.99)
     update_losses = ema.apply([discrim_loss, gen_loss_GAN, gen_loss_L1])
 
-    global_step = tf.contrib.framework.get_or_create_global_step()
+    global_step = tf.train.get_or_create_global_step()
     incr_global_step = tf.assign(global_step, global_step+1)
 
     return Model(
@@ -539,9 +519,6 @@ def append_index(filesets, step=False):
 
 
 def main():
-    if tf.__version__.split('.')[0] != "1":
-        raise Exception("Tensorflow version 1 required")
-
     if a.seed is None:
         a.seed = random.randint(0, 2**31 - 1)
 
@@ -741,7 +718,6 @@ def main():
                 for i, f in enumerate(filesets):
                     print("evaluated image", f["name"])
                 index_path = append_index(filesets)
-
             print("wrote index at", index_path)
         else:
             # training
diff --git a/server/Dockerfile b/server/Dockerfile
deleted file mode 100644
index ad9910fe50227ccadbb17c918cd1764d05ba4561..0000000000000000000000000000000000000000
--- a/server/Dockerfile
+++ /dev/null
@@ -1,39 +0,0 @@
-FROM ubuntu:xenial-20170119
-
-RUN apt-get update
-
-RUN apt-get install -y --no-install-recommends \
-        python-dev \
-        python-pip \
-        python-setuptools \
-        python-wheel
-
-RUN pip install \
-    scipy==0.18.1 \
-    appdirs==1.4.0 \
-    funcsigs==1.0.2 \
-    google-api-python-client==1.6.2 \
-    google-auth==0.7.0 \
-    google-auth-httplib2==0.0.2 \
-    google-cloud-core==0.22.1 \
-    google-cloud-storage==0.22.0 \
-    googleapis-common-protos==1.5.2 \
-    httplib2==0.10.3 \
-    mock==2.0.0 \
-    numpy==1.12.0 \
-    oauth2client==4.0.0 \
-    packaging==16.8 \
-    pbr==1.10.0 \
-    protobuf==3.2.0 \
-    pyasn1==0.2.2 \
-    pyasn1-modules==0.0.8 \
-    pyparsing==2.1.10 \
-    rsa==3.4.2 \
-    six==1.10.0 \
-    uritemplate==3.0.0 \
-    tensorflow==1.0.0
-
-WORKDIR /server
-COPY models models
-COPY static static
-COPY serve.py serve.py
\ No newline at end of file
diff --git a/server/README.md b/server/README.md
index f5fac9be602501ece825ff5be1a19c45dc635c15..2993d3497ff3e4e330687505bafe5d8242bbb76e 100644
--- a/server/README.md
+++ b/server/README.md
@@ -2,193 +2,28 @@
 
 Host pix2pix-tensorflow models to be used with something like the [Image-to-Image Demo](https://affinelayer.com/pixsrv/).
 
-This is a simple python server that serves models exported from `pix2pix.py --mode export`.  It can serve local models or use [Cloud ML](https://cloud.google.com/ml/) to run the model.
+This is a simple python server that uses [deeplearn.js](https://deeplearnjs.org/) exported from pix2pix checkpoints using `tools/export-checkpoint.py`.
 
 ## Exporting
 
-You can export a model to be served with `--mode export`. As with testing, you should specify the checkpoint to use with `--checkpoint`.
+You can export a model to be served with `tools/export-checkpoint.py`.
 
 ```sh
-python ../pix2pix.py \
-  --mode export \
-  --output_dir models/facades \
-  --checkpoint ../facades_train
+python tools/export-checkpoint.py \
+  --checkpoint facades_BtoA \
+  --output_file static/models/facades_BtoA.bin
 ```
 
-## Local Serving
-
-Using the [pix2pix-tensorflow Docker image](https://hub.docker.com/r/affinelayer/pix2pix-tensorflow/):
-
-```sh
-# export a model to upload (if you did not export one above)
-python ../tools/dockrun.py python tools/export-example-model.py --output_dir models/example
-# process an image with the model using local tensorflow
-python ../tools/dockrun.py python tools/process-local.py \
-    --model_dir models/example \
-    --input_file static/facades-input.png \
-    --output_file output.png
-# run local server
-python ../tools/dockrun.py --port 8000 python serve.py --port 8000 --local_models_dir models
-# test the local server
-python tools/process-remote.py \
-    --input_file static/facades-input.png \
-    --url http://localhost:8000/example \
-    --output_file output.png
-```
-
-If you open [http://localhost:8000/](http://localhost:8000/) in a browser, you should see an interactive demo, though this expects the server to be hosting the exported models available here:
-
-- [edges2shoes](https://mega.nz/#!HtYwAZTY!5tBLYt_6HFj9u2Kxgp4-I36O4EV9r3bDP44ztX3qesI)
-- [edges2handbags](https://mega.nz/#!Clg3EaLA!YW2jfRHvwpJn5Elww_wM-f3eRzKiGHLw-F4A3eQCceI)
-- [facades](https://mega.nz/#!f1ZjmZoa!mCSxFRxt1WLBpNFsv5raoroEigxomDVpdi40aOG1KMc)
-
-Extract those to the models directory and restart the server to have it host the models.
-
-## Cloud ML Serving
-
-For this you'll want to generate a service account JSON file from https://console.cloud.google.com/iam-admin/serviceaccounts/project (select "Furnish a new private key").  If you are already logged in with the gcloud SDK, the script will auto-detect credentials from that if you leave off the `--credentials` option.
+You can also copy models from the `pix2pix-tensorflow-models` repo:
 
 ```sh
-# upload model to google cloud ml
-python ../tools/dockrun.py python tools/upload-model.py \
-    --bucket your-models-bucket-name-here \
-    --model_name example \
-    --model_dir models/example \
-    --credentials service-account.json
-# process an image with the model using google cloud ml
-python ../tools/dockrun.py python tools/process-cloud.py \
-    --model example \
-    --input_file static/facades-input.png \
-    --output_file output.png \
-    --credentials service-account.json
+git clone git@github.com:affinelayer/pix2pix-tensorflow-models.git static/models
 ```
 
-## Running serve.py on Google Cloud Platform
-
-Assuming you have gcloud and docker setup:
+## Serving
 
 ```sh
-export GOOGLE_PROJECT=<project name>
-# build image
-# make sure models are in a directory called "models" in the current directory
-sudo docker build --rm --tag us.gcr.io/$GOOGLE_PROJECT/pix2pix-server .
-# test image locally
-sudo docker run --publish 8080:8080 --rm --name server us.gcr.io/$GOOGLE_PROJECT/pix2pix-server python -u serve.py \
-    --port 8080 \
-    --local_models_dir models
-python tools/process-remote.py \
-    --input_file static/facades-input.png \
-    --url http://localhost:8080/example \
-    --output_file output.png
-# publish image to private google container repository
-python tools/upload-image.py --project $GOOGLE_PROJECT --version v1
-# setup server
-cp terraform.tfvars.example terraform.tfvars
-# edit terraform.tfvars to put your cloud info in there
-python ../tools/dockrun.py terraform plan
-python ../tools/dockrun.py terraform apply
+python serve.py --port 8000
 ```
 
-## Full training + exporting + hosting commands
-
-Tested with Python 3.6, Tensorflow 1.0.0, Docker, gcloud, and Terraform (https://www.terraform.io/downloads.html)
-
-```sh
-git clone https://github.com/affinelayer/pix2pix-tensorflow.git
-cd pix2pix-tensorflow
-
-# get some images (only 2 for testing)
-mkdir source
-curl -o source/cat1.jpg https://farm5.staticflickr.com/4032/4394955222_eea73818d9_o.jpg
-curl -o source/cat2.jpg http://wallpapercave.com/wp/ePMeSmp.jpg
-
-# resize source images
-python tools/process.py \
-  --input_dir source \
-  --operation resize \
-  --output_dir resized
-
-# create edges from resized images (uses docker container since compiling the dependencies is annoying)
-python tools/dockrun.py python tools/process.py \
-  --input_dir resized \
-  --operation edges \
-  --output_dir edges
-
-# combine resized with edges
-python tools/process.py \
-  --input_dir edges \
-  --b_dir resized \
-  --operation combine \
-  --output_dir combined
-
-# train on images (only 1 epoch for testing)
-python pix2pix.py \
-  --mode train \
-  --output_dir train \
-  --max_epochs 1 \
-  --input_dir combined \
-  --which_direction AtoB
-
-# export model (creates a version of the model that works with the server in server/serve.py as well as google hosted tensorflow)
-python pix2pix.py \
-  --mode export \
-  --output_dir server/models/edges2cats_AtoB \
-  --checkpoint train
-
-# process image locally using exported model
-python server/tools/process-local.py \
-    --model_dir server/models/edges2cats_AtoB \
-    --input_file edges/cat1.png \
-    --output_file output.png
-
-# serve model locally
-cd server
-python serve.py --port 8000 --local_models_dir models
-
-# open http://localhost:8000 in a browser, and scroll to the bottom, you should be able to process an edges2cat image and get a bunch of noise as output
-
-# serve model remotely
-
-export GOOGLE_PROJECT=<project name>
-
-# build image
-# make sure models are in a directory called "models" in the current directory
-docker build --rm --tag us.gcr.io/$GOOGLE_PROJECT/pix2pix-server .
-
-# test image locally
-docker run --publish 8000:8000 --rm --name server us.gcr.io/$GOOGLE_PROJECT/pix2pix-server python -u serve.py \
-    --port 8000 \
-    --local_models_dir models
-
-# run this while the above server is running
-python tools/process-remote.py \
-    --input_file static/edges2cats-input.png \
-    --url http://localhost:8000/edges2cats_AtoB \
-    --output_file output.png
-
-# publish image to private google container repository
-python tools/upload-image.py --project $GOOGLE_PROJECT --version v1
-
-# create a google cloud server
-cp terraform.tfvars.example terraform.tfvars
-# edit terraform.tfvars to put your cloud info in there
-# get the service-account.json from the google cloud console
-# make sure GCE is enabled on your account as well
-python terraform plan
-python terraform apply
-
-# get name of server
-gcloud compute instance-groups list-instances pix2pix-manager
-# ssh to server
-gcloud compute ssh <name of instance here>
-# look at the logs (can take awhile to load docker image)
-sudo journalctl -f -u pix2pix
-# if you have never made an http-server before, apparently you may need this rule
-gcloud compute firewall-rules create http-server --allow=tcp:80 --target-tags http-server
-# get ip address of load balancer
-gcloud compute forwarding-rules list
-# open that in the browser, should see the same page you saw locally
-
-# to destroy the GCP resources, use this
-terraform destroy
-```
\ No newline at end of file
+If you open [http://localhost:8000/](http://localhost:8000/) in a browser, you should see an interactive demo.
diff --git a/server/deployment.tf b/server/deployment.tf
deleted file mode 100644
index 228dbe87661eb2572a0d5d840c88e1b7903cf301..0000000000000000000000000000000000000000
--- a/server/deployment.tf
+++ /dev/null
@@ -1,134 +0,0 @@
-variable "google_project" {}
-variable "google_credentials_file" {}
-variable "server_image_version" {}
-
-provider "google" {
-  region      = "us-central1"
-  credentials = "${file(var.google_credentials_file)}"
-  project     = "${var.google_project}"
-}
-
-# cluster
-
-resource "google_compute_instance_template" "cluster" {
-  name_prefix  = "pix2pix-template-"
-  machine_type = "n1-highcpu-2"
-
-  tags           = ["http-server"]
-  can_ip_forward = false
-
-  scheduling {
-    automatic_restart   = true
-    on_host_maintenance = "MIGRATE"
-  }
-
-  disk {
-    source_image = "cos-cloud/cos-stable"
-  }
-
-  network_interface {
-    network = "default"
-
-    access_config {
-      // Ephemeral IP
-    }
-  }
-
-  metadata {
-    user-data = <<EOF
-#cloud-config
-
-users:
-- name: pix2pix
-  uid: 2000
-
-write_files:
-- path: /etc/systemd/system/pix2pix.service
-  permissions: 0644
-  owner: root
-  content: |
-    [Unit]
-    Description=Run pix2pix
-
-    [Service]
-    Environment="HOME=/home/pix2pix"
-    ExecStartPre=/usr/share/google/dockercfg_update.sh
-    ExecStart=/usr/bin/docker run --rm --log-driver=gcplogs -u 2000 --publish 80:8080 --name=pix2pix us.gcr.io/${var.google_project}/pix2pix-server:${var.server_image_version} python -u serve.py --port 8080 --local_models_dir models --origin https://affinelayer.com
-    ExecStop=/usr/bin/docker stop pix2pix
-    ExecStopPost=/usr/bin/docker rm pix2pix
-    Restart=always
-    RestartSec=30
-
-runcmd:
-- iptables -A INPUT -p tcp --dport 80 -j ACCEPT
-- systemctl daemon-reload
-- systemctl start pix2pix.service
-EOF
-  }
-
-  service_account {
-    scopes = ["https://www.googleapis.com/auth/logging.write", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/cloud-platform"]
-  }
-
-  lifecycle {
-    create_before_destroy = true
-  }
-}
-
-resource "google_compute_http_health_check" "cluster" {
-  name         = "pix2pix-check"
-  request_path = "/health"
-
-  timeout_sec         = 5
-  check_interval_sec  = 10
-  unhealthy_threshold = 3
-}
-
-resource "google_compute_target_pool" "cluster" {
-  name = "pix2pix-pool"
-
-  health_checks = [
-    "${google_compute_http_health_check.cluster.name}",
-  ]
-}
-
-resource "google_compute_instance_group_manager" "cluster" {
-  name               = "pix2pix-manager"
-  instance_template  = "${google_compute_instance_template.cluster.self_link}"
-  base_instance_name = "pix2pix"
-  zone               = "us-central1-c"
-
-  target_pools = ["${google_compute_target_pool.cluster.self_link}"]
-
-  // don't update instances with terraform, which supposedly can't do a rolling restart
-  // use this to update them instead:
-  // gcloud compute instance-groups managed recreate-instances pix2pix-manager --zone us-central1-c --instances pix2pix-frfh
-  update_strategy = "NONE"
-}
-
-resource "google_compute_address" "cluster" {
-  name = "pix2pix-cluster"
-}
-
-resource "google_compute_forwarding_rule" "cluster" {
-  name       = "pix2pix-balancer"
-  target     = "${google_compute_target_pool.cluster.self_link}"
-  port_range = "80-80"
-  ip_address = "${google_compute_address.cluster.address}"
-}
-
-resource "google_compute_autoscaler" "cluster" {
-  name   = "pix2pix-autoscaler"
-  zone   = "us-central1-c"
-  target = "${google_compute_instance_group_manager.cluster.self_link}"
-
-  autoscaling_policy = {
-    max_replicas    = 8
-    min_replicas    = 1
-    cooldown_period = 60
-
-    cpu_utilization {
-      target = 0.7
-    }
-  }
-}
diff --git a/server/serve.py b/server/serve.py
index f0241fe93d35868f3d92061a7916ea1163007a32..e6fb921c47cb29771e169be6890dfad730c46a3a 100644
--- a/server/serve.py
+++ b/server/serve.py
@@ -1,298 +1,18 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import socket
-import time
-import argparse
-import base64
 import os
-import json
-import traceback
-import threading
-import multiprocessing
-import random
-
-
-# https://github.com/Nakiami/MultithreadedSimpleHTTPServer/blob/master/MultithreadedSimpleHTTPServer.py
-try:
-    # Python 2
-    from SocketServer import ThreadingMixIn
-    from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
-except ImportError:
-    # Python 3
-    from socketserver import ThreadingMixIn
-    from http.server import HTTPServer, BaseHTTPRequestHandler
-
-socket.setdefaulttimeout(30)
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--local_models_dir", help="directory containing local models to serve (either this or --cloud_model_names must be specified)")
-parser.add_argument("--cloud_model_names", help="comma separated list of cloud models to serve (either this or --local_models_dir must be specified)")
-parser.add_argument("--origin", help="allowed origin")
-parser.add_argument("--addr", default="", help="address to listen on")
-parser.add_argument("--port", default=8000, type=int, help="port to listen on")
-parser.add_argument("--wait", default=0, type=int, help="time to wait for each request")
-parser.add_argument("--credentials", help="JSON credentials for a Google Cloud Platform service account, generate this at https://console.cloud.google.com/iam-admin/serviceaccounts/project (select \"Furnish a new private key\")")
-parser.add_argument("--project", help="Google Cloud Project to use, only necessary if using default application credentials")
-a = parser.parse_args()
-
-jobs = threading.Semaphore(multiprocessing.cpu_count() * 4)
-models = {}
-ml = None
-project_id = None
-build_cloud_client = None
-
-
-class RateCounter(object):
-    def __init__(self, window_us, granularity=1000):
-        self.granularity = granularity
-        self.width_us = int(window_us) // self.granularity
-        self.buckets = [0] * self.granularity
-        self.updated = 0
-        self.lock = threading.RLock()
-
-    def incr(self, amt=1):
-        with self.lock:
-            now_us = int(time.time() * 1e6)
-            now = now_us // self.width_us
-
-            if now > self.updated:
-                # need to clear any buckets between the update time and now
-                if now - self.updated > self.granularity:
-                    self.buckets = [0] * self.granularity
-                    self.updated = now
-                else:
-                    while self.updated <= now:
-                        self.updated += 1
-                        self.buckets[self.updated % self.granularity] = 0
-
-            self.buckets[now % self.granularity] += amt
-
-    def value(self):
-        with self.lock:
-            # update any expired buckets
-            self.incr(amt=0)
-            return sum(self.buckets)
-
-
-successes = RateCounter(1 * 60 * 1e6)
-failures = RateCounter(1 * 60 * 1e6)
-cloud_requests = RateCounter(5 * 60 * 1e6)
-cloud_accepts = RateCounter(5 * 60 * 1e6)
-
-
-class Handler(BaseHTTPRequestHandler):
-    def do_GET(self):
-        if self.path == "/health":
-            self.send_response(200)
-            self.end_headers()
-            self.wfile.write("OK")
-            return
-
-        if not os.path.exists("static"):
-            self.send_response(404)
-            return
-
-        if self.path == "/":
-            self.send_response(200)
-            self.send_header("Content-Type", "text/html")
-            self.end_headers()
-            with open("static/index.html", "rb") as f:
-                self.wfile.write(f.read())
-            return
-
-        filenames = [name for name in os.listdir("static") if not name.startswith(".")]
-        path = self.path[1:]
-        if path not in filenames:
-            self.send_response(404)
-            return
-
-        self.send_response(200)
-        if path.endswith(".png"):
-            self.send_header("Content-Type", "image/png")
-        elif path.endswith(".jpg"):
-            self.send_header("Content-Type", "image/jpeg")
-        else:
-            self.send_header("Content-Type", "application/octet-stream")
-        self.end_headers()
-        with open("static/" + path, "rb") as f:
-            self.wfile.write(f.read())
-
-
-    def do_OPTIONS(self):
-        self.send_response(200)
-        if "origin" in self.headers:
-            if a.origin is not None and self.headers["origin"] != a.origin:
-                print("invalid origin %s" % self.headers["origin"])
-                self.send_response(400)
-                return
-            self.send_header("access-control-allow-origin", self.headers["origin"])
-
-        allow_headers = self.headers.get("access-control-request-headers", "*")
-        self.send_header("access-control-allow-headers", allow_headers)
-        self.send_header("access-control-allow-methods", "POST, OPTIONS")
-        self.send_header("access-control-max-age", "3600")
-        self.end_headers()
-
-
-    def do_POST(self):
-        start = time.time()
-
-        status = 200
-        headers = {}
-        if "origin" in self.headers:
-            headers = {"access-control-allow-origin": self.headers["origin"]}
-        body = ""
-
-        try:
-            name = self.path[1:]
-
-            if name not in models:
-                raise Exception("invalid model")
-
-            variants = models[name]  # "cloud" and "local" are the two possible variants
-
-            content_len = int(self.headers.get("content-length", "0"))
-            if content_len > 1 * 1024 * 1024:
-                raise Exception("post body too large")
-            input_data = self.rfile.read(content_len)
-            input_b64data = base64.urlsafe_b64encode(input_data)
-
-            time.sleep(a.wait)
-
-            cloud_reject_prob = max(0, (cloud_requests.value() - 1.1 * cloud_accepts.value()) / (cloud_requests.value() + 1))
-            # print("requests=%d accepts=%d cloud_reject_prob=%f" % (cloud_requests.value(), cloud_accepts.value(), cloud_reject_prob))
-
-            output_b64data = None
-            if "cloud" in variants and random.random() > cloud_reject_prob:
-                input_instance = dict(input=input_b64data, key="0")
-                # the client does not seem to be threadsafe, so make one for each request
-                # also the cache is broken by oauth2client 4.0.0, so use a memory cache
-                request = build_cloud_client().projects().predict(name="projects/" + project_id + "/models/" + name, body={"instances": [input_instance]})
-                try:
-                    cloud_requests.incr()
-                    response = request.execute()
-                    output_instance = response["predictions"][0]
-                    output_b64data = output_instance["output"].encode("ascii")
-                    cloud_accepts.incr()
-                except Exception as e:
-                    print("exception while running cloud model", traceback.format_exc())
-                    print("falling back to local")
-
-            if output_b64data is None and "local" in variants and jobs.acquire(blocking=False):
-                m = variants["local"]
-                try:
-                    output_b64data = m["sess"].run(m["output"], feed_dict={m["input"]: [input_b64data]})[0]
-                finally:
-                    jobs.release()
-
-            if output_b64data is None:
-                raise Exception("too many requests")
-
-            # add any missing padding
-            output_b64data += b"=" * (-len(output_b64data) % 4)
-            output_data = base64.urlsafe_b64decode(output_b64data)
-            if output_data.startswith(b"\x89PNG"):
-                headers["content-type"] = "image/png"
-            else:
-                headers["content-type"] = "image/jpeg"
-            body = output_data
-            successes.incr()
-        except Exception as e:
-            failures.incr()
-            print("exception", traceback.format_exc())
-            status = 500
-            body = "server error"
-
-        self.send_response(status)
-        for key, value in headers.items():
-            self.send_header(key, value)
-        self.end_headers()
-        self.wfile.write(body)
-
-        print("finished in %0.1fs successes=%d failures=%d" % (time.time() - start, successes.value(), failures.value()))
-
-
-class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
-    pass
+import argparse
+from http.server import HTTPServer, SimpleHTTPRequestHandler
 
 
 def main():
-    if a.local_models_dir is None and a.cloud_model_names is None:
-        raise Exception("must specify --local_models_dir or --cloud_model_names")
-
-    if a.local_models_dir is not None:
-        import tensorflow as tf
-        for name in os.listdir(a.local_models_dir):
-            if name.startswith("."):
-                continue
-
-            print("loading model", name)
-
-            with tf.Graph().as_default() as graph:
-                sess = tf.Session(graph=graph)
-                saver = tf.train.import_meta_graph(os.path.join(a.local_models_dir, name, "export.meta"))
-
-                saver.restore(sess, os.path.join(a.local_models_dir, name, "export"))
-                input_vars = json.loads(tf.get_collection("inputs")[0])
-                output_vars = json.loads(tf.get_collection("outputs")[0])
-                input = graph.get_tensor_by_name(input_vars["input"])
-                output = graph.get_tensor_by_name(output_vars["output"])
-
-                if name not in models:
-                    models[name] = {}
-
-                models[name]["local"] = dict(
-                    sess=sess,
-                    input=input,
-                    output=output,
-                )
-
-    if a.cloud_model_names is not None:
-        import oauth2client.service_account
-        import googleapiclient.discovery
-        import googleapiclient.discovery_cache.base
-        import httplib2
-
-        for name in a.cloud_model_names.split(","):
-            if name not in models:
-                models[name] = {}
-            models[name]["cloud"] = None
-
-        scopes = ["https://www.googleapis.com/auth/cloud-platform"]
-        global project_id
-        if a.credentials is None:
-            credentials = oauth2client.client.GoogleCredentials.get_application_default()
-            # use this only to detect the project
-            import google.cloud.storage
-            storage = google.cloud.storage.Client()
-            project_id = storage.project
-            if a.project is not None:
-                project_id = a.project
-        else:
-            credentials = oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name(a.credentials, scopes)
-            with open(a.credentials, "r") as f:
-                project_id = json.loads(f.read())["project_id"]
-
-        # due to what appears to be a bug, we cannot get the discovery document when specifying an http client
-        # so grab it first, then the second build should use the cache
-        class Cache(googleapiclient.discovery_cache.base.Cache):
-            def __init__(self):
-                self.cache = {}
-
-            def get(self, url):
-                return self.cache.get(url)
-
-            def set(self, url, content):
-                self.cache[url] = content
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--port", default=8000, type=int, help="port to listen on")
+    args = parser.parse_args()
 
-        cache = Cache()
-        googleapiclient.discovery.build("ml", "v1beta1", credentials=credentials, cache=cache)
-        global build_cloud_client
-        build_cloud_client = lambda: googleapiclient.discovery.build("ml", "v1beta1", http=credentials.authorize(httplib2.Http(timeout=10)), cache=cache)
+    os.chdir('static')
+    server_address = ('', args.port)
+    httpd = HTTPServer(server_address, SimpleHTTPRequestHandler)
+    print('serving at http://127.0.0.1:%d' % args.port)
+    httpd.serve_forever()
 
-    print("listening on %s:%s" % (a.addr, a.port))
-    ThreadedHTTPServer((a.addr, a.port), Handler).serve_forever()
 
 main()
diff --git a/server/static/deeplearn-0.3.15.js b/server/static/deeplearn-0.3.15.js
new file mode 100644
index 0000000000000000000000000000000000000000..f2d6e9fd5075c8df43860df35a0a8aec71c8f32b
--- /dev/null
+++ b/server/static/deeplearn-0.3.15.js
@@ -0,0 +1,12900 @@
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.deeplearn = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+
+},{}],2:[function(require,module,exports){
+// A library of seedable RNGs implemented in Javascript.
+//
+// Usage:
+//
+// var seedrandom = require('seedrandom');
+// var random = seedrandom(1); // or any seed.
+// var x = random();       // 0 <= x < 1.  Every bit is random.
+// var x = random.quick(); // 0 <= x < 1.  32 bits of randomness.
+
+// alea, a 53-bit multiply-with-carry generator by Johannes Baagøe.
+// Period: ~2^116
+// Reported to pass all BigCrush tests.
+var alea = require('./lib/alea');
+
+// xor128, a pure xor-shift generator by George Marsaglia.
+// Period: 2^128-1.
+// Reported to fail: MatrixRank and LinearComp.
+var xor128 = require('./lib/xor128');
+
+// xorwow, George Marsaglia's 160-bit xor-shift combined plus weyl.
+// Period: 2^192-2^32
+// Reported to fail: CollisionOver, SimpPoker, and LinearComp.
+var xorwow = require('./lib/xorwow');
+
+// xorshift7, by François Panneton and Pierre L'ecuyer, takes
+// a different approach: it adds robustness by allowing more shifts
+// than Marsaglia's original three.  It is a 7-shift generator
+// with 256 bits, that passes BigCrush with no systmatic failures.
+// Period 2^256-1.
+// No systematic BigCrush failures reported.
+var xorshift7 = require('./lib/xorshift7');
+
+// xor4096, by Richard Brent, is a 4096-bit xor-shift with a
+// very long period that also adds a Weyl generator. It also passes
+// BigCrush with no systematic failures.  Its long period may
+// be useful if you have many generators and need to avoid
+// collisions.
+// Period: 2^4128-2^32.
+// No systematic BigCrush failures reported.
+var xor4096 = require('./lib/xor4096');
+
+// Tyche-i, by Samuel Neves and Filipe Araujo, is a bit-shifting random
+// number generator derived from ChaCha, a modern stream cipher.
+// https://eden.dei.uc.pt/~sneves/pubs/2011-snfa2.pdf
+// Period: ~2^127
+// No systematic BigCrush failures reported.
+var tychei = require('./lib/tychei');
+
+// The original ARC4-based prng included in this library.
+// Period: ~2^1600
+var sr = require('./seedrandom');
+
+sr.alea = alea;
+sr.xor128 = xor128;
+sr.xorwow = xorwow;
+sr.xorshift7 = xorshift7;
+sr.xor4096 = xor4096;
+sr.tychei = tychei;
+
+module.exports = sr;
+
+},{"./lib/alea":3,"./lib/tychei":4,"./lib/xor128":5,"./lib/xor4096":6,"./lib/xorshift7":7,"./lib/xorwow":8,"./seedrandom":9}],3:[function(require,module,exports){
+// A port of an algorithm by Johannes Baagøe <baagoe@baagoe.com>, 2010
+// http://baagoe.com/en/RandomMusings/javascript/
+// https://github.com/nquinlan/better-random-numbers-for-javascript-mirror
+// Original work is under MIT license -
+
+// Copyright (C) 2010 by Johannes Baagøe <baagoe@baagoe.org>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+// 
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+// 
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+
+
+(function(global, module, define) {
+
+function Alea(seed) {
+  var me = this, mash = Mash();
+
+  me.next = function() {
+    var t = 2091639 * me.s0 + me.c * 2.3283064365386963e-10; // 2^-32
+    me.s0 = me.s1;
+    me.s1 = me.s2;
+    return me.s2 = t - (me.c = t | 0);
+  };
+
+  // Apply the seeding algorithm from Baagoe.
+  me.c = 1;
+  me.s0 = mash(' ');
+  me.s1 = mash(' ');
+  me.s2 = mash(' ');
+  me.s0 -= mash(seed);
+  if (me.s0 < 0) { me.s0 += 1; }
+  me.s1 -= mash(seed);
+  if (me.s1 < 0) { me.s1 += 1; }
+  me.s2 -= mash(seed);
+  if (me.s2 < 0) { me.s2 += 1; }
+  mash = null;
+}
+
+function copy(f, t) {
+  t.c = f.c;
+  t.s0 = f.s0;
+  t.s1 = f.s1;
+  t.s2 = f.s2;
+  return t;
+}
+
+function impl(seed, opts) {
+  var xg = new Alea(seed),
+      state = opts && opts.state,
+      prng = xg.next;
+  prng.int32 = function() { return (xg.next() * 0x100000000) | 0; }
+  prng.double = function() {
+    return prng() + (prng() * 0x200000 | 0) * 1.1102230246251565e-16; // 2^-53
+  };
+  prng.quick = prng;
+  if (state) {
+    if (typeof(state) == 'object') copy(state, xg);
+    prng.state = function() { return copy(xg, {}); }
+  }
+  return prng;
+}
+
+function Mash() {
+  var n = 0xefc8249d;
+
+  var mash = function(data) {
+    data = data.toString();
+    for (var i = 0; i < data.length; i++) {
+      n += data.charCodeAt(i);
+      var h = 0.02519603282416938 * n;
+      n = h >>> 0;
+      h -= n;
+      h *= n;
+      n = h >>> 0;
+      h -= n;
+      n += h * 0x100000000; // 2^32
+    }
+    return (n >>> 0) * 2.3283064365386963e-10; // 2^-32
+  };
+
+  return mash;
+}
+
+
+if (module && module.exports) {
+  module.exports = impl;
+} else if (define && define.amd) {
+  define(function() { return impl; });
+} else {
+  this.alea = impl;
+}
+
+})(
+  this,
+  (typeof module) == 'object' && module,    // present in node.js
+  (typeof define) == 'function' && define   // present with an AMD loader
+);
+
+
+
+},{}],4:[function(require,module,exports){
+// A Javascript implementaion of the "Tyche-i" prng algorithm by
+// Samuel Neves and Filipe Araujo.
+// See https://eden.dei.uc.pt/~sneves/pubs/2011-snfa2.pdf
+
+(function(global, module, define) {
+
+function XorGen(seed) {
+  var me = this, strseed = '';
+
+  // Set up generator function.
+  me.next = function() {
+    var b = me.b, c = me.c, d = me.d, a = me.a;
+    b = (b << 25) ^ (b >>> 7) ^ c;
+    c = (c - d) | 0;
+    d = (d << 24) ^ (d >>> 8) ^ a;
+    a = (a - b) | 0;
+    me.b = b = (b << 20) ^ (b >>> 12) ^ c;
+    me.c = c = (c - d) | 0;
+    me.d = (d << 16) ^ (c >>> 16) ^ a;
+    return me.a = (a - b) | 0;
+  };
+
+  /* The following is non-inverted tyche, which has better internal
+   * bit diffusion, but which is about 25% slower than tyche-i in JS.
+  me.next = function() {
+    var a = me.a, b = me.b, c = me.c, d = me.d;
+    a = (me.a + me.b | 0) >>> 0;
+    d = me.d ^ a; d = d << 16 ^ d >>> 16;
+    c = me.c + d | 0;
+    b = me.b ^ c; b = b << 12 ^ d >>> 20;
+    me.a = a = a + b | 0;
+    d = d ^ a; me.d = d = d << 8 ^ d >>> 24;
+    me.c = c = c + d | 0;
+    b = b ^ c;
+    return me.b = (b << 7 ^ b >>> 25);
+  }
+  */
+
+  me.a = 0;
+  me.b = 0;
+  me.c = 2654435769 | 0;
+  me.d = 1367130551;
+
+  if (seed === Math.floor(seed)) {
+    // Integer seed.
+    me.a = (seed / 0x100000000) | 0;
+    me.b = seed | 0;
+  } else {
+    // String seed.
+    strseed += seed;
+  }
+
+  // Mix in string seed, then discard an initial batch of 64 values.
+  for (var k = 0; k < strseed.length + 20; k++) {
+    me.b ^= strseed.charCodeAt(k) | 0;
+    me.next();
+  }
+}
+
+function copy(f, t) {
+  t.a = f.a;
+  t.b = f.b;
+  t.c = f.c;
+  t.d = f.d;
+  return t;
+};
+
+function impl(seed, opts) {
+  var xg = new XorGen(seed),
+      state = opts && opts.state,
+      prng = function() { return (xg.next() >>> 0) / 0x100000000; };
+  prng.double = function() {
+    do {
+      var top = xg.next() >>> 11,
+          bot = (xg.next() >>> 0) / 0x100000000,
+          result = (top + bot) / (1 << 21);
+    } while (result === 0);
+    return result;
+  };
+  prng.int32 = xg.next;
+  prng.quick = prng;
+  if (state) {
+    if (typeof(state) == 'object') copy(state, xg);
+    prng.state = function() { return copy(xg, {}); }
+  }
+  return prng;
+}
+
+if (module && module.exports) {
+  module.exports = impl;
+} else if (define && define.amd) {
+  define(function() { return impl; });
+} else {
+  this.tychei = impl;
+}
+
+})(
+  this,
+  (typeof module) == 'object' && module,    // present in node.js
+  (typeof define) == 'function' && define   // present with an AMD loader
+);
+
+
+
+},{}],5:[function(require,module,exports){
+// A Javascript implementaion of the "xor128" prng algorithm by
+// George Marsaglia.  See http://www.jstatsoft.org/v08/i14/paper
+
+(function(global, module, define) {
+
+function XorGen(seed) {
+  var me = this, strseed = '';
+
+  me.x = 0;
+  me.y = 0;
+  me.z = 0;
+  me.w = 0;
+
+  // Set up generator function.
+  me.next = function() {
+    var t = me.x ^ (me.x << 11);
+    me.x = me.y;
+    me.y = me.z;
+    me.z = me.w;
+    return me.w ^= (me.w >>> 19) ^ t ^ (t >>> 8);
+  };
+
+  if (seed === (seed | 0)) {
+    // Integer seed.
+    me.x = seed;
+  } else {
+    // String seed.
+    strseed += seed;
+  }
+
+  // Mix in string seed, then discard an initial batch of 64 values.
+  for (var k = 0; k < strseed.length + 64; k++) {
+    me.x ^= strseed.charCodeAt(k) | 0;
+    me.next();
+  }
+}
+
+function copy(f, t) {
+  t.x = f.x;
+  t.y = f.y;
+  t.z = f.z;
+  t.w = f.w;
+  return t;
+}
+
+function impl(seed, opts) {
+  var xg = new XorGen(seed),
+      state = opts && opts.state,
+      prng = function() { return (xg.next() >>> 0) / 0x100000000; };
+  prng.double = function() {
+    do {
+      var top = xg.next() >>> 11,
+          bot = (xg.next() >>> 0) / 0x100000000,
+          result = (top + bot) / (1 << 21);
+    } while (result === 0);
+    return result;
+  };
+  prng.int32 = xg.next;
+  prng.quick = prng;
+  if (state) {
+    if (typeof(state) == 'object') copy(state, xg);
+    prng.state = function() { return copy(xg, {}); }
+  }
+  return prng;
+}
+
+if (module && module.exports) {
+  module.exports = impl;
+} else if (define && define.amd) {
+  define(function() { return impl; });
+} else {
+  this.xor128 = impl;
+}
+
+})(
+  this,
+  (typeof module) == 'object' && module,    // present in node.js
+  (typeof define) == 'function' && define   // present with an AMD loader
+);
+
+
+
+},{}],6:[function(require,module,exports){
+// A Javascript implementaion of Richard Brent's Xorgens xor4096 algorithm.
+//
+// This fast non-cryptographic random number generator is designed for
+// use in Monte-Carlo algorithms. It combines a long-period xorshift
+// generator with a Weyl generator, and it passes all common batteries
+// of stasticial tests for randomness while consuming only a few nanoseconds
+// for each prng generated.  For background on the generator, see Brent's
+// paper: "Some long-period random number generators using shifts and xors."
+// http://arxiv.org/pdf/1004.3115v1.pdf
+//
+// Usage:
+//
+// var xor4096 = require('xor4096');
+// random = xor4096(1);                        // Seed with int32 or string.
+// assert.equal(random(), 0.1520436450538547); // (0, 1) range, 53 bits.
+// assert.equal(random.int32(), 1806534897);   // signed int32, 32 bits.
+//
+// For nonzero numeric keys, this impelementation provides a sequence
+// identical to that by Brent's xorgens 3 implementaion in C.  This
+// implementation also provides for initalizing the generator with
+// string seeds, or for saving and restoring the state of the generator.
+//
+// On Chrome, this prng benchmarks about 2.1 times slower than
+// Javascript's built-in Math.random().
+
+(function(global, module, define) {
+
+function XorGen(seed) {
+  var me = this;
+
+  // Set up generator function.
+  me.next = function() {
+    var w = me.w,
+        X = me.X, i = me.i, t, v;
+    // Update Weyl generator.
+    me.w = w = (w + 0x61c88647) | 0;
+    // Update xor generator.
+    v = X[(i + 34) & 127];
+    t = X[i = ((i + 1) & 127)];
+    v ^= v << 13;
+    t ^= t << 17;
+    v ^= v >>> 15;
+    t ^= t >>> 12;
+    // Update Xor generator array state.
+    v = X[i] = v ^ t;
+    me.i = i;
+    // Result is the combination.
+    return (v + (w ^ (w >>> 16))) | 0;
+  };
+
+  function init(me, seed) {
+    var t, v, i, j, w, X = [], limit = 128;
+    if (seed === (seed | 0)) {
+      // Numeric seeds initialize v, which is used to generates X.
+      v = seed;
+      seed = null;
+    } else {
+      // String seeds are mixed into v and X one character at a time.
+      seed = seed + '\0';
+      v = 0;
+      limit = Math.max(limit, seed.length);
+    }
+    // Initialize circular array and weyl value.
+    for (i = 0, j = -32; j < limit; ++j) {
+      // Put the unicode characters into the array, and shuffle them.
+      if (seed) v ^= seed.charCodeAt((j + 32) % seed.length);
+      // After 32 shuffles, take v as the starting w value.
+      if (j === 0) w = v;
+      v ^= v << 10;
+      v ^= v >>> 15;
+      v ^= v << 4;
+      v ^= v >>> 13;
+      if (j >= 0) {
+        w = (w + 0x61c88647) | 0;     // Weyl.
+        t = (X[j & 127] ^= (v + w));  // Combine xor and weyl to init array.
+        i = (0 == t) ? i + 1 : 0;     // Count zeroes.
+      }
+    }
+    // We have detected all zeroes; make the key nonzero.
+    if (i >= 128) {
+      X[(seed && seed.length || 0) & 127] = -1;
+    }
+    // Run the generator 512 times to further mix the state before using it.
+    // Factoring this as a function slows the main generator, so it is just
+    // unrolled here.  The weyl generator is not advanced while warming up.
+    i = 127;
+    for (j = 4 * 128; j > 0; --j) {
+      v = X[(i + 34) & 127];
+      t = X[i = ((i + 1) & 127)];
+      v ^= v << 13;
+      t ^= t << 17;
+      v ^= v >>> 15;
+      t ^= t >>> 12;
+      X[i] = v ^ t;
+    }
+    // Storing state as object members is faster than using closure variables.
+    me.w = w;
+    me.X = X;
+    me.i = i;
+  }
+
+  init(me, seed);
+}
+
+function copy(f, t) {
+  t.i = f.i;
+  t.w = f.w;
+  t.X = f.X.slice();
+  return t;
+};
+
+function impl(seed, opts) {
+  if (seed == null) seed = +(new Date);
+  var xg = new XorGen(seed),
+      state = opts && opts.state,
+      prng = function() { return (xg.next() >>> 0) / 0x100000000; };
+  prng.double = function() {
+    do {
+      var top = xg.next() >>> 11,
+          bot = (xg.next() >>> 0) / 0x100000000,
+          result = (top + bot) / (1 << 21);
+    } while (result === 0);
+    return result;
+  };
+  prng.int32 = xg.next;
+  prng.quick = prng;
+  if (state) {
+    if (state.X) copy(state, xg);
+    prng.state = function() { return copy(xg, {}); }
+  }
+  return prng;
+}
+
+if (module && module.exports) {
+  module.exports = impl;
+} else if (define && define.amd) {
+  define(function() { return impl; });
+} else {
+  this.xor4096 = impl;
+}
+
+})(
+  this,                                     // window object or global
+  (typeof module) == 'object' && module,    // present in node.js
+  (typeof define) == 'function' && define   // present with an AMD loader
+);
+
+},{}],7:[function(require,module,exports){
+// A Javascript implementaion of the "xorshift7" algorithm by
+// François Panneton and Pierre L'ecuyer:
+// "On the Xorgshift Random Number Generators"
+// http://saluc.engr.uconn.edu/refs/crypto/rng/panneton05onthexorshift.pdf
+
+(function(global, module, define) {
+
+function XorGen(seed) {
+  var me = this;
+
+  // Set up generator function.
+  me.next = function() {
+    // Update xor generator.
+    var X = me.x, i = me.i, t, v, w;
+    t = X[i]; t ^= (t >>> 7); v = t ^ (t << 24);
+    t = X[(i + 1) & 7]; v ^= t ^ (t >>> 10);
+    t = X[(i + 3) & 7]; v ^= t ^ (t >>> 3);
+    t = X[(i + 4) & 7]; v ^= t ^ (t << 7);
+    t = X[(i + 7) & 7]; t = t ^ (t << 13); v ^= t ^ (t << 9);
+    X[i] = v;
+    me.i = (i + 1) & 7;
+    return v;
+  };
+
+  function init(me, seed) {
+    var j, w, X = [];
+
+    if (seed === (seed | 0)) {
+      // Seed state array using a 32-bit integer.
+      w = X[0] = seed;
+    } else {
+      // Seed state using a string.
+      seed = '' + seed;
+      for (j = 0; j < seed.length; ++j) {
+        X[j & 7] = (X[j & 7] << 15) ^
+            (seed.charCodeAt(j) + X[(j + 1) & 7] << 13);
+      }
+    }
+    // Enforce an array length of 8, not all zeroes.
+    while (X.length < 8) X.push(0);
+    for (j = 0; j < 8 && X[j] === 0; ++j);
+    if (j == 8) w = X[7] = -1; else w = X[j];
+
+    me.x = X;
+    me.i = 0;
+
+    // Discard an initial 256 values.
+    for (j = 256; j > 0; --j) {
+      me.next();
+    }
+  }
+
+  init(me, seed);
+}
+
+function copy(f, t) {
+  t.x = f.x.slice();
+  t.i = f.i;
+  return t;
+}
+
+function impl(seed, opts) {
+  if (seed == null) seed = +(new Date);
+  var xg = new XorGen(seed),
+      state = opts && opts.state,
+      prng = function() { return (xg.next() >>> 0) / 0x100000000; };
+  prng.double = function() {
+    do {
+      var top = xg.next() >>> 11,
+          bot = (xg.next() >>> 0) / 0x100000000,
+          result = (top + bot) / (1 << 21);
+    } while (result === 0);
+    return result;
+  };
+  prng.int32 = xg.next;
+  prng.quick = prng;
+  if (state) {
+    if (state.x) copy(state, xg);
+    prng.state = function() { return copy(xg, {}); }
+  }
+  return prng;
+}
+
+if (module && module.exports) {
+  module.exports = impl;
+} else if (define && define.amd) {
+  define(function() { return impl; });
+} else {
+  this.xorshift7 = impl;
+}
+
+})(
+  this,
+  (typeof module) == 'object' && module,    // present in node.js
+  (typeof define) == 'function' && define   // present with an AMD loader
+);
+
+
+},{}],8:[function(require,module,exports){
+// A Javascript implementaion of the "xorwow" prng algorithm by
+// George Marsaglia.  See http://www.jstatsoft.org/v08/i14/paper
+
+(function(global, module, define) {
+
+function XorGen(seed) {
+  var me = this, strseed = '';
+
+  // Set up generator function.
+  me.next = function() {
+    var t = (me.x ^ (me.x >>> 2));
+    me.x = me.y; me.y = me.z; me.z = me.w; me.w = me.v;
+    return (me.d = (me.d + 362437 | 0)) +
+       (me.v = (me.v ^ (me.v << 4)) ^ (t ^ (t << 1))) | 0;
+  };
+
+  me.x = 0;
+  me.y = 0;
+  me.z = 0;
+  me.w = 0;
+  me.v = 0;
+
+  if (seed === (seed | 0)) {
+    // Integer seed.
+    me.x = seed;
+  } else {
+    // String seed.
+    strseed += seed;
+  }
+
+  // Mix in string seed, then discard an initial batch of 64 values.
+  for (var k = 0; k < strseed.length + 64; k++) {
+    me.x ^= strseed.charCodeAt(k) | 0;
+    if (k == strseed.length) {
+      me.d = me.x << 10 ^ me.x >>> 4;
+    }
+    me.next();
+  }
+}
+
+function copy(f, t) {
+  t.x = f.x;
+  t.y = f.y;
+  t.z = f.z;
+  t.w = f.w;
+  t.v = f.v;
+  t.d = f.d;
+  return t;
+}
+
+function impl(seed, opts) {
+  var xg = new XorGen(seed),
+      state = opts && opts.state,
+      prng = function() { return (xg.next() >>> 0) / 0x100000000; };
+  prng.double = function() {
+    do {
+      var top = xg.next() >>> 11,
+          bot = (xg.next() >>> 0) / 0x100000000,
+          result = (top + bot) / (1 << 21);
+    } while (result === 0);
+    return result;
+  };
+  prng.int32 = xg.next;
+  prng.quick = prng;
+  if (state) {
+    if (typeof(state) == 'object') copy(state, xg);
+    prng.state = function() { return copy(xg, {}); }
+  }
+  return prng;
+}
+
+if (module && module.exports) {
+  module.exports = impl;
+} else if (define && define.amd) {
+  define(function() { return impl; });
+} else {
+  this.xorwow = impl;
+}
+
+})(
+  this,
+  (typeof module) == 'object' && module,    // present in node.js
+  (typeof define) == 'function' && define   // present with an AMD loader
+);
+
+
+
+},{}],9:[function(require,module,exports){
+/*
+Copyright 2014 David Bau.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+*/
+
+(function (pool, math) {
+//
+// The following constants are related to IEEE 754 limits.
+//
+var global = this,
+    width = 256,        // each RC4 output is 0 <= x < 256
+    chunks = 6,         // at least six RC4 outputs for each double
+    digits = 52,        // there are 52 significant digits in a double
+    rngname = 'random', // rngname: name for Math.random and Math.seedrandom
+    startdenom = math.pow(width, chunks),
+    significance = math.pow(2, digits),
+    overflow = significance * 2,
+    mask = width - 1,
+    nodecrypto;         // node.js crypto module, initialized at the bottom.
+
+//
+// seedrandom()
+// This is the seedrandom function described above.
+//
+function seedrandom(seed, options, callback) {
+  var key = [];
+  options = (options == true) ? { entropy: true } : (options || {});
+
+  // Flatten the seed string or build one from local entropy if needed.
+  var shortseed = mixkey(flatten(
+    options.entropy ? [seed, tostring(pool)] :
+    (seed == null) ? autoseed() : seed, 3), key);
+
+  // Use the seed to initialize an ARC4 generator.
+  var arc4 = new ARC4(key);
+
+  // This function returns a random double in [0, 1) that contains
+  // randomness in every bit of the mantissa of the IEEE 754 value.
+  var prng = function() {
+    var n = arc4.g(chunks),             // Start with a numerator n < 2 ^ 48
+        d = startdenom,                 //   and denominator d = 2 ^ 48.
+        x = 0;                          //   and no 'extra last byte'.
+    while (n < significance) {          // Fill up all significant digits by
+      n = (n + x) * width;              //   shifting numerator and
+      d *= width;                       //   denominator and generating a
+      x = arc4.g(1);                    //   new least-significant-byte.
+    }
+    while (n >= overflow) {             // To avoid rounding up, before adding
+      n /= 2;                           //   last byte, shift everything
+      d /= 2;                           //   right using integer math until
+      x >>>= 1;                         //   we have exactly the desired bits.
+    }
+    return (n + x) / d;                 // Form the number within [0, 1).
+  };
+
+  prng.int32 = function() { return arc4.g(4) | 0; }
+  prng.quick = function() { return arc4.g(4) / 0x100000000; }
+  prng.double = prng;
+
+  // Mix the randomness into accumulated entropy.
+  mixkey(tostring(arc4.S), pool);
+
+  // Calling convention: what to return as a function of prng, seed, is_math.
+  return (options.pass || callback ||
+      function(prng, seed, is_math_call, state) {
+        if (state) {
+          // Load the arc4 state from the given state if it has an S array.
+          if (state.S) { copy(state, arc4); }
+          // Only provide the .state method if requested via options.state.
+          prng.state = function() { return copy(arc4, {}); }
+        }
+
+        // If called as a method of Math (Math.seedrandom()), mutate
+        // Math.random because that is how seedrandom.js has worked since v1.0.
+        if (is_math_call) { math[rngname] = prng; return seed; }
+
+        // Otherwise, it is a newer calling convention, so return the
+        // prng directly.
+        else return prng;
+      })(
+  prng,
+  shortseed,
+  'global' in options ? options.global : (this == math),
+  options.state);
+}
+math['seed' + rngname] = seedrandom;
+
+//
+// ARC4
+//
+// An ARC4 implementation.  The constructor takes a key in the form of
+// an array of at most (width) integers that should be 0 <= x < (width).
+//
+// The g(count) method returns a pseudorandom integer that concatenates
+// the next (count) outputs from ARC4.  Its return value is a number x
+// that is in the range 0 <= x < (width ^ count).
+//
+function ARC4(key) {
+  var t, keylen = key.length,
+      me = this, i = 0, j = me.i = me.j = 0, s = me.S = [];
+
+  // The empty key [] is treated as [0].
+  if (!keylen) { key = [keylen++]; }
+
+  // Set up S using the standard key scheduling algorithm.
+  while (i < width) {
+    s[i] = i++;
+  }
+  for (i = 0; i < width; i++) {
+    s[i] = s[j = mask & (j + key[i % keylen] + (t = s[i]))];
+    s[j] = t;
+  }
+
+  // The "g" method returns the next (count) outputs as one number.
+  (me.g = function(count) {
+    // Using instance members instead of closure state nearly doubles speed.
+    var t, r = 0,
+        i = me.i, j = me.j, s = me.S;
+    while (count--) {
+      t = s[i = mask & (i + 1)];
+      r = r * width + s[mask & ((s[i] = s[j = mask & (j + t)]) + (s[j] = t))];
+    }
+    me.i = i; me.j = j;
+    return r;
+    // For robust unpredictability, the function call below automatically
+    // discards an initial batch of values.  This is called RC4-drop[256].
+    // See http://google.com/search?q=rsa+fluhrer+response&btnI
+  })(width);
+}
+
+//
+// copy()
+// Copies internal state of ARC4 to or from a plain object.
+//
+function copy(f, t) {
+  t.i = f.i;
+  t.j = f.j;
+  t.S = f.S.slice();
+  return t;
+};
+
+//
+// flatten()
+// Converts an object tree to nested arrays of strings.
+//
+function flatten(obj, depth) {
+  var result = [], typ = (typeof obj), prop;
+  if (depth && typ == 'object') {
+    for (prop in obj) {
+      try { result.push(flatten(obj[prop], depth - 1)); } catch (e) {}
+    }
+  }
+  return (result.length ? result : typ == 'string' ? obj : obj + '\0');
+}
+
+//
+// mixkey()
+// Mixes a string seed into a key that is an array of integers, and
+// returns a shortened string seed that is equivalent to the result key.
+//
+function mixkey(seed, key) {
+  var stringseed = seed + '', smear, j = 0;
+  while (j < stringseed.length) {
+    key[mask & j] =
+      mask & ((smear ^= key[mask & j] * 19) + stringseed.charCodeAt(j++));
+  }
+  return tostring(key);
+}
+
+//
+// autoseed()
+// Returns an object for autoseeding, using window.crypto and Node crypto
+// module if available.
+//
+function autoseed() {
+  try {
+    var out;
+    if (nodecrypto && (out = nodecrypto.randomBytes)) {
+      // The use of 'out' to remember randomBytes makes tight minified code.
+      out = out(width);
+    } else {
+      out = new Uint8Array(width);
+      (global.crypto || global.msCrypto).getRandomValues(out);
+    }
+    return tostring(out);
+  } catch (e) {
+    var browser = global.navigator,
+        plugins = browser && browser.plugins;
+    return [+new Date, global, plugins, global.screen, tostring(pool)];
+  }
+}
+
+//
+// tostring()
+// Converts an array of charcodes to a string
+//
+function tostring(a) {
+  return String.fromCharCode.apply(0, a);
+}
+
+//
+// When seedrandom.js is loaded, we immediately mix a few bits
+// from the built-in RNG into the entropy pool.  Because we do
+// not want to interfere with deterministic PRNG state later,
+// seedrandom will not call math.random on its own again after
+// initialization.
+//
+mixkey(math.random(), pool);
+
+//
+// Nodejs and AMD support: export the implementation as a module using
+// either convention.
+//
+if ((typeof module) == 'object' && module.exports) {
+  module.exports = seedrandom;
+  // When in node.js, try using crypto package for autoseeding.
+  try {
+    nodecrypto = require('crypto');
+  } catch (ex) {}
+} else if ((typeof define) == 'function' && define.amd) {
+  define(function() { return seedrandom; });
+}
+
+// End anonymous scope, and pass initial values.
+})(
+  [],     // pool: entropy pool starts empty
+  Math    // math: package containing random, pow, and seedrandom
+);
+
+},{"crypto":1}],10:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../math/ndarray");
+var MANIFEST_FILE = 'manifest.json';
+var CheckpointLoader = (function () {
+    function CheckpointLoader(urlPath) {
+        this.urlPath = urlPath;
+        if (this.urlPath.charAt(this.urlPath.length - 1) !== '/') {
+            this.urlPath += '/';
+        }
+    }
+    CheckpointLoader.prototype.loadManifest = function () {
+        var _this = this;
+        return new Promise(function (resolve, reject) {
+            var xhr = new XMLHttpRequest();
+            xhr.open('GET', _this.urlPath + MANIFEST_FILE);
+            xhr.onload = function () {
+                _this.checkpointManifest = JSON.parse(xhr.responseText);
+                resolve();
+            };
+            xhr.onerror = function (error) {
+                throw new Error(MANIFEST_FILE + " not found at " + _this.urlPath + ". " + error);
+            };
+            xhr.send();
+        });
+    };
+    CheckpointLoader.prototype.getCheckpointManifest = function () {
+        var _this = this;
+        if (this.checkpointManifest == null) {
+            return new Promise(function (resolve, reject) {
+                _this.loadManifest().then(function () {
+                    resolve(_this.checkpointManifest);
+                });
+            });
+        }
+        return new Promise(function (resolve, reject) {
+            resolve(_this.checkpointManifest);
+        });
+    };
+    CheckpointLoader.prototype.getAllVariables = function () {
+        var _this = this;
+        if (this.variables != null) {
+            return new Promise(function (resolve, reject) {
+                resolve(_this.variables);
+            });
+        }
+        return new Promise(function (resolve, reject) {
+            _this.getCheckpointManifest().then(function (checkpointDefinition) {
+                var variableNames = Object.keys(_this.checkpointManifest);
+                var variablePromises = [];
+                for (var i = 0; i < variableNames.length; i++) {
+                    variablePromises.push(_this.getVariable(variableNames[i]));
+                }
+                Promise.all(variablePromises).then(function (variables) {
+                    _this.variables = {};
+                    for (var i = 0; i < variables.length; i++) {
+                        _this.variables[variableNames[i]] = variables[i];
+                    }
+                    resolve(_this.variables);
+                });
+            });
+        });
+    };
+    CheckpointLoader.prototype.getVariable = function (varName) {
+        var _this = this;
+        if (!(varName in this.checkpointManifest)) {
+            throw new Error('Cannot load non-existant variable ' + varName);
+        }
+        var variableRequestPromiseMethod = function (resolve, reject) {
+            var xhr = new XMLHttpRequest();
+            xhr.responseType = 'arraybuffer';
+            var fname = _this.checkpointManifest[varName].filename;
+            xhr.open('GET', _this.urlPath + fname);
+            xhr.onload = function () {
+                if (xhr.status === 404) {
+                    throw new Error("Not found variable " + varName);
+                }
+                var values = new Float32Array(xhr.response);
+                var ndarray = ndarray_1.NDArray.make(_this.checkpointManifest[varName].shape, { values: values });
+                resolve(ndarray);
+            };
+            xhr.onerror = function (error) {
+                throw new Error("Could not fetch variable " + varName + ": " + error);
+            };
+            xhr.send();
+        };
+        if (this.checkpointManifest == null) {
+            return new Promise(function (resolve, reject) {
+                _this.loadManifest().then(function () {
+                    new Promise(variableRequestPromiseMethod).then(resolve);
+                });
+            });
+        }
+        return new Promise(variableRequestPromiseMethod);
+    };
+    return CheckpointLoader;
+}());
+exports.CheckpointLoader = CheckpointLoader;
+
+},{"../math/ndarray":95}],11:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../math/ndarray");
+var util = require("../util");
+var STATS_SAMPLE_PERCENTAGE = 0.1;
+var InMemoryDataset = (function () {
+    function InMemoryDataset(dataShapes, math) {
+        this.dataShapes = dataShapes;
+        this.math = math;
+        this.normalizationInfo = {};
+    }
+    InMemoryDataset.prototype.getDataShape = function (dataIndex) {
+        return this.dataShapes[dataIndex];
+    };
+    InMemoryDataset.prototype.getData = function () {
+        return this.dataset;
+    };
+    InMemoryDataset.prototype.getStats = function () {
+        var _this = this;
+        if (this.dataset == null) {
+            throw new Error('Data is null.');
+        }
+        return this.dataset.map(function (d) { return _this.getStatsForData(d); });
+    };
+    InMemoryDataset.prototype.getStatsForData = function (data) {
+        var inputMin = Number.POSITIVE_INFINITY;
+        var inputMax = Number.NEGATIVE_INFINITY;
+        var exampleIndices = data.map(function (example, i) { return i; });
+        util.shuffle(exampleIndices);
+        exampleIndices =
+            exampleIndices.slice(exampleIndices.length * STATS_SAMPLE_PERCENTAGE);
+        for (var i = 0; i < exampleIndices.length; i++) {
+            var inputValues = data[exampleIndices[i]].getValues();
+            for (var j = 0; j < inputValues.length; j++) {
+                inputMin = Math.min(inputMin, inputValues[j]);
+                inputMax = Math.max(inputMax, inputValues[j]);
+            }
+        }
+        return {
+            inputMin: inputMin,
+            inputMax: inputMax,
+            exampleCount: data.length,
+            shape: data[0].shape,
+        };
+    };
+    InMemoryDataset.prototype.normalizeExamplesToRange = function (examples, curLowerBounds, curUpperBounds, newLowerBounds, newUpperBounds) {
+        var _this = this;
+        var curBoundsIsPerDimension = (curUpperBounds instanceof Float32Array &&
+            curLowerBounds instanceof Float32Array);
+        var newBoundsIsPerDimension = (newLowerBounds instanceof Float32Array &&
+            newUpperBounds instanceof Float32Array);
+        var inputSize = util.sizeFromShape(examples[0].shape);
+        var newExamples = [];
+        examples.forEach(function (example) {
+            var inputValues = example.getValues();
+            var normalizedValues = new Float32Array(inputSize);
+            for (var j = 0; j < inputSize; j++) {
+                var curLowerBound = curBoundsIsPerDimension ?
+                    curLowerBounds[j] :
+                    curLowerBounds;
+                var curUpperBound = curBoundsIsPerDimension ?
+                    curUpperBounds[j] :
+                    curUpperBounds;
+                var curRange = curUpperBound - curLowerBound;
+                var newLowerBound = newBoundsIsPerDimension ?
+                    newLowerBounds[j] :
+                    newLowerBounds;
+                var newUpperBound = newBoundsIsPerDimension ?
+                    newUpperBounds[j] :
+                    newUpperBounds;
+                var newRange = newUpperBound - newLowerBound;
+                if (curRange === 0) {
+                    normalizedValues[j] = newLowerBound;
+                }
+                else {
+                    normalizedValues[j] = newLowerBound +
+                        newRange * (inputValues[j] - curLowerBound) / curRange;
+                }
+            }
+            newExamples.push(ndarray_1.NDArray.make(example.shape, { values: normalizedValues }, 'float32', _this.math));
+        });
+        return newExamples;
+    };
+    InMemoryDataset.prototype.computeBounds = function (dataIndex) {
+        var _this = this;
+        if (this.dataset == null) {
+            throw new Error('Data is null.');
+        }
+        var size = util.sizeFromShape(this.dataset[dataIndex][0].shape);
+        this.normalizationInfo[dataIndex] = {
+            isNormalized: false,
+            minValues: new Float32Array(size),
+            maxValues: new Float32Array(size)
+        };
+        for (var i = 0; i < size; i++) {
+            this.normalizationInfo[dataIndex].minValues[i] = Number.POSITIVE_INFINITY;
+            this.normalizationInfo[dataIndex].maxValues[i] = Number.NEGATIVE_INFINITY;
+        }
+        this.dataset[dataIndex].forEach(function (example) {
+            var inputValues = example.getValues();
+            for (var k = 0; k < size; k++) {
+                _this.normalizationInfo[dataIndex].minValues[k] = Math.min(_this.normalizationInfo[dataIndex].minValues[k], inputValues[k]);
+                _this.normalizationInfo[dataIndex].maxValues[k] = Math.max(_this.normalizationInfo[dataIndex].maxValues[k], inputValues[k]);
+            }
+        });
+    };
+    InMemoryDataset.prototype.normalizeWithinBounds = function (dataIndex, lowerBound, upperBound) {
+        if (this.dataset == null) {
+            throw new Error('Data is null.');
+        }
+        if (dataIndex >= this.dataset.length) {
+            throw new Error('dataIndex out of bounds.');
+        }
+        if (this.normalizationInfo[dataIndex] == null) {
+            this.computeBounds(dataIndex);
+        }
+        var curLowerBounds;
+        var curUpperBounds;
+        if (this.normalizationInfo[dataIndex].isNormalized) {
+            curLowerBounds = this.normalizationInfo[dataIndex].lowerBound;
+            curUpperBounds = this.normalizationInfo[dataIndex].upperBound;
+        }
+        else {
+            curLowerBounds = this.normalizationInfo[dataIndex].minValues;
+            curUpperBounds = this.normalizationInfo[dataIndex].maxValues;
+        }
+        this.dataset[dataIndex] = this.normalizeExamplesToRange(this.dataset[dataIndex], curLowerBounds, curUpperBounds, lowerBound, upperBound);
+        this.normalizationInfo[dataIndex].isNormalized = true;
+        this.normalizationInfo[dataIndex].lowerBound = lowerBound;
+        this.normalizationInfo[dataIndex].upperBound = upperBound;
+    };
+    InMemoryDataset.prototype.isNormalized = function (dataIndex) {
+        return this.normalizationInfo != null &&
+            this.normalizationInfo[dataIndex].isNormalized;
+    };
+    InMemoryDataset.prototype.removeNormalization = function (dataIndex) {
+        if (this.dataset == null) {
+            throw new Error('Training or test data is null.');
+        }
+        if (!this.isNormalized(dataIndex)) {
+            return;
+        }
+        this.dataset[dataIndex] = this.normalizeExamplesToRange(this.dataset[dataIndex], this.normalizationInfo[dataIndex].lowerBound, this.normalizationInfo[dataIndex].upperBound, this.normalizationInfo[dataIndex].minValues, this.normalizationInfo[dataIndex].maxValues);
+        this.normalizationInfo[dataIndex].isNormalized = false;
+    };
+    InMemoryDataset.prototype.unnormalizeExamples = function (examples, dataIndex) {
+        if (!this.isNormalized(dataIndex)) {
+            return examples;
+        }
+        return this.normalizeExamplesToRange(examples, this.normalizationInfo[dataIndex].lowerBound, this.normalizationInfo[dataIndex].upperBound, this.normalizationInfo[dataIndex].minValues, this.normalizationInfo[dataIndex].maxValues);
+    };
+    InMemoryDataset.prototype.dispose = function () {
+        if (this.dataset == null) {
+            return;
+        }
+        for (var i = 0; i < this.dataset.length; i++) {
+            for (var j = 0; j < this.dataset[i].length; j++) {
+                this.dataset[i][j].dispose();
+            }
+        }
+        this.dataset = [];
+    };
+    return InMemoryDataset;
+}());
+exports.InMemoryDataset = InMemoryDataset;
+
+},{"../math/ndarray":95,"../util":101}],12:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../math/ndarray");
+var util = require("../util");
+var InMemoryShuffledInputProviderBuilder = (function () {
+    function InMemoryShuffledInputProviderBuilder(inputs) {
+        this.inputs = inputs;
+        this.idx = 0;
+        this.inputCounter = 0;
+        this.epoch = 0;
+        this.shuffledIndices = util.createShuffledIndices(inputs[0].length);
+        this.numInputs = inputs.length;
+        var numExamples = this.inputs[0].length;
+        for (var i = 0; i < this.numInputs; i++) {
+            util.assert(this.inputs[i].length === numExamples, 'Number of examples must match across different inputs.');
+        }
+        for (var i = 0; i < this.numInputs; i++) {
+            var inputShape = this.inputs[i][0].shape;
+            for (var j = 0; j < this.inputs[i].length; j++) {
+                util.assertShapesMatch(inputShape, this.inputs[i][j].shape);
+            }
+        }
+    }
+    InMemoryShuffledInputProviderBuilder.prototype.getCurrentExampleIndex = function () {
+        var returnIdx = this.idx;
+        this.inputCounter++;
+        if (this.inputCounter >= this.numInputs) {
+            this.idx++;
+            this.inputCounter = 0;
+            if (this.idx >= this.inputs[0].length) {
+                this.idx = 0;
+                this.epoch++;
+            }
+        }
+        return returnIdx;
+    };
+    InMemoryShuffledInputProviderBuilder.prototype.getNextInput = function (inputId) {
+        var currentExampleIndex = this.getCurrentExampleIndex();
+        return this.inputs[inputId][this.shuffledIndices[currentExampleIndex]];
+    };
+    InMemoryShuffledInputProviderBuilder.prototype.getEpoch = function () {
+        return this.epoch;
+    };
+    InMemoryShuffledInputProviderBuilder.prototype.getInputProviders = function () {
+        var inputProviders = [];
+        for (var i = 0; i < this.numInputs; i++) {
+            inputProviders.push(this.getInputProvider(i));
+        }
+        return inputProviders;
+    };
+    return InMemoryShuffledInputProviderBuilder;
+}());
+exports.InMemoryShuffledInputProviderBuilder = InMemoryShuffledInputProviderBuilder;
+var InCPUMemoryShuffledInputProviderBuilder = (function (_super) {
+    __extends(InCPUMemoryShuffledInputProviderBuilder, _super);
+    function InCPUMemoryShuffledInputProviderBuilder() {
+        return _super !== null && _super.apply(this, arguments) || this;
+    }
+    InCPUMemoryShuffledInputProviderBuilder.prototype.getInputProvider = function (inputId) {
+        var shuffledInputProvider = this;
+        return {
+            getNextCopy: function (math) {
+                return ndarray_1.NDArray.like(shuffledInputProvider.getNextInput(inputId));
+            },
+            disposeCopy: function (math, copy) {
+                copy.dispose();
+            }
+        };
+    };
+    return InCPUMemoryShuffledInputProviderBuilder;
+}(InMemoryShuffledInputProviderBuilder));
+exports.InCPUMemoryShuffledInputProviderBuilder = InCPUMemoryShuffledInputProviderBuilder;
+var InGPUMemoryShuffledInputProviderBuilder = (function (_super) {
+    __extends(InGPUMemoryShuffledInputProviderBuilder, _super);
+    function InGPUMemoryShuffledInputProviderBuilder() {
+        return _super !== null && _super.apply(this, arguments) || this;
+    }
+    InGPUMemoryShuffledInputProviderBuilder.prototype.getInputProvider = function (inputId) {
+        var shuffledInputProvider = this;
+        return {
+            getNextCopy: function (math) {
+                return math.clone(shuffledInputProvider.getNextInput(inputId));
+            },
+            disposeCopy: function (math, copy) {
+                copy.dispose();
+            }
+        };
+    };
+    return InGPUMemoryShuffledInputProviderBuilder;
+}(InMemoryShuffledInputProviderBuilder));
+exports.InGPUMemoryShuffledInputProviderBuilder = InGPUMemoryShuffledInputProviderBuilder;
+
+},{"../math/ndarray":95,"../util":101}],13:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var math_1 = require("../math/math");
+var ndarray_1 = require("../math/ndarray");
+var util = require("../util");
+var dataset_1 = require("./dataset");
+var PARSING_IMAGE_CANVAS_HEIGHT_PX = 1000;
+function getXhrDatasetConfig(jsonConfigPath) {
+    return new Promise(function (resolve, reject) {
+        var xhr = new XMLHttpRequest();
+        xhr.open('GET', jsonConfigPath);
+        xhr.onload = function () {
+            resolve(JSON.parse(xhr.responseText));
+        };
+        xhr.onerror = function (error) {
+            reject(error);
+        };
+        xhr.send();
+    });
+}
+exports.getXhrDatasetConfig = getXhrDatasetConfig;
+var XhrDataset = (function (_super) {
+    __extends(XhrDataset, _super);
+    function XhrDataset(xhrDatasetConfig) {
+        var _this = this;
+        var safeMode = false;
+        _this = _super.call(this, xhrDatasetConfig.data.map(function (x) { return x.shape; }), new math_1.NDArrayMath('cpu', safeMode)) || this;
+        _this.xhrDatasetConfig = xhrDatasetConfig;
+        return _this;
+    }
+    XhrDataset.prototype.getNDArray = function (info) {
+        var _this = this;
+        var dataPromise = info.dataType === 'png' ?
+            parseTypedArrayFromPng(info, info.shape) :
+            parseTypedArrayFromBinary(info);
+        var inputSize = util.sizeFromShape(info.shape);
+        return dataPromise.then(function (data) {
+            var ndarrays = [];
+            for (var i = 0; i < data.length / inputSize; i++) {
+                var values = data.subarray(i * inputSize, (i + 1) * inputSize);
+                var ndarray = ndarray_1.NDArray.make(info.shape, { values: new Float32Array(values) }, 'float32', _this.math);
+                ndarrays.push(ndarray);
+            }
+            return ndarrays;
+        });
+    };
+    XhrDataset.prototype.fetchData = function () {
+        var _this = this;
+        return new Promise(function (resolve, reject) {
+            var promises = _this.xhrDatasetConfig.data.map(function (x) { return _this.getNDArray(x); });
+            Promise.all(promises).then(function (data) {
+                _this.dataset = data;
+                resolve();
+            });
+        });
+    };
+    return XhrDataset;
+}(dataset_1.InMemoryDataset));
+exports.XhrDataset = XhrDataset;
+function parseTypedArrayFromBinary(info) {
+    return new Promise(function (resolve, reject) {
+        var xhr = new XMLHttpRequest();
+        xhr.open('GET', info.path);
+        xhr.responseType = 'arraybuffer';
+        xhr.onload = function (event) {
+            var data = (info.dataType === 'float32') ?
+                new Float32Array(xhr.response) :
+                new Uint8Array(xhr.response);
+            resolve(data);
+        };
+        xhr.onerror = function (err) { return reject(err); };
+        xhr.send();
+    });
+}
+function parseGrayscaleImageData(data, result, resultOffset) {
+    var idx = resultOffset;
+    for (var i = 0; i < data.length; i += 4) {
+        result[idx++] = data[i];
+    }
+}
+function parseRGBImageData(data, result, resultOffset) {
+    var idx = resultOffset;
+    for (var i = 0; i < data.length; i += 4) {
+        result[idx] = data[i];
+        result[idx + 1] = data[i + 1];
+        result[idx + 2] = data[i + 2];
+        idx += 3;
+    }
+}
+function parseImage(img, shape) {
+    var canvas = document.createElement('canvas');
+    var ctx = canvas.getContext('2d');
+    var N = img.height;
+    var inputSize = util.sizeFromShape(shape);
+    var result = new Uint8Array(N * inputSize);
+    if (img.width !== shape[0] * shape[1]) {
+        throw new Error("Image width (" + img.width + ") must be multiple of " +
+            ("rows*columns (" + shape[0] + "*" + shape[1] + ") of the ndarray"));
+    }
+    canvas.width = img.width;
+    canvas.height = PARSING_IMAGE_CANVAS_HEIGHT_PX;
+    var sx = 0;
+    var sWidth = canvas.width;
+    var sHeight = canvas.height;
+    var dx = 0;
+    var dy = 0;
+    var dWidth = sWidth;
+    var dHeight = sHeight;
+    var depth = shape[2];
+    var offset = 0;
+    var numPasses = Math.ceil(N / canvas.height);
+    for (var pass = 0; pass < numPasses; ++pass) {
+        var sy = pass * canvas.height;
+        if ((pass === numPasses - 1) && (N % canvas.height > 0)) {
+            canvas.height = N % canvas.height;
+            sHeight = canvas.height;
+            dHeight = sHeight;
+        }
+        ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
+        var data = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+        (depth === 1) ? parseGrayscaleImageData(data, result, offset) :
+            parseRGBImageData(data, result, offset);
+        offset += canvas.height * inputSize;
+    }
+    return result;
+}
+function parseTypedArrayFromPng(info, shape) {
+    return new Promise(function (resolve, reject) {
+        var img = new Image();
+        img.setAttribute('crossOrigin', '');
+        img.onload = function () {
+            var result = parseImage(img, shape);
+            img.src = '';
+            img = null;
+            resolve(result);
+        };
+        img.src = info.path;
+    });
+}
+
+},{"../math/math":94,"../math/ndarray":95,"../util":101,"./dataset":11}],14:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+function isMobile() {
+    var a = navigator.userAgent || navigator.vendor || window.opera;
+    return /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i
+        .test(a) ||
+        /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i
+            .test(a.substr(0, 4));
+}
+exports.isMobile = isMobile;
+
+},{}],15:[function(require,module,exports){
+(function (global){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var device_util = require("./device_util");
+var math_1 = require("./math/math");
+var util = require("./util");
+var Type;
+(function (Type) {
+    Type[Type["NUMBER"] = 0] = "NUMBER";
+    Type[Type["BOOLEAN"] = 1] = "BOOLEAN";
+})(Type = exports.Type || (exports.Type = {}));
+exports.URL_PROPERTIES = [
+    { name: 'WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_ENABLED', type: Type.BOOLEAN },
+    { name: 'WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_RELIABLE', type: Type.BOOLEAN },
+    { name: 'WEBGL_VERSION', type: Type.NUMBER },
+    { name: 'WEBGL_FLOAT_TEXTURE_ENABLED', type: Type.BOOLEAN }, {
+        name: 'WEBGL_GET_BUFFER_SUB_DATA_ASYNC_EXTENSION_ENABLED',
+        type: Type.BOOLEAN
+    }
+];
+function getWebGLRenderingContext(webGLVersion) {
+    if (webGLVersion === 0) {
+        throw new Error('Cannot get WebGL rendering context, WebGL is disabled.');
+    }
+    var tempCanvas = document.createElement('canvas');
+    if (webGLVersion === 1) {
+        return (tempCanvas.getContext('webgl') ||
+            tempCanvas.getContext('experimental-webgl'));
+    }
+    return tempCanvas.getContext('webgl2');
+}
+function loseContext(gl) {
+    if (gl != null) {
+        var loseContextExtension = gl.getExtension('WEBGL_lose_context');
+        if (loseContextExtension == null) {
+            throw new Error('Extension WEBGL_lose_context not supported on this browser.');
+        }
+        loseContextExtension.loseContext();
+    }
+}
+function isWebGLVersionEnabled(webGLVersion) {
+    var gl = getWebGLRenderingContext(webGLVersion);
+    if (gl != null) {
+        loseContext(gl);
+        return true;
+    }
+    return false;
+}
+function isWebGLDisjointQueryTimerEnabled(webGLVersion) {
+    var gl = getWebGLRenderingContext(webGLVersion);
+    var extensionName = webGLVersion === 1 ? 'EXT_disjoint_timer_query' :
+        'EXT_disjoint_timer_query_webgl2';
+    var ext = gl.getExtension(extensionName);
+    var isExtEnabled = ext != null;
+    if (gl != null) {
+        loseContext(gl);
+    }
+    return isExtEnabled;
+}
+function isFloatTextureReadPixelsEnabled(webGLVersion) {
+    if (webGLVersion === 0) {
+        return false;
+    }
+    var gl = getWebGLRenderingContext(webGLVersion);
+    if (webGLVersion === 1) {
+        if (gl.getExtension('OES_texture_float') == null) {
+            return false;
+        }
+    }
+    else {
+        if (gl.getExtension('EXT_color_buffer_float') == null) {
+            return false;
+        }
+    }
+    var frameBuffer = gl.createFramebuffer();
+    var texture = gl.createTexture();
+    gl.bindTexture(gl.TEXTURE_2D, texture);
+    var internalFormat = webGLVersion === 2 ? gl.RGBA32F : gl.RGBA;
+    gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 1, 1, 0, gl.RGBA, gl.FLOAT, null);
+    gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer);
+    gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
+    var frameBufferComplete = (gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE);
+    gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.FLOAT, new Float32Array(4));
+    var readPixelsNoError = gl.getError() === gl.NO_ERROR;
+    loseContext(gl);
+    return frameBufferComplete && readPixelsNoError;
+}
+function isWebGLGetBufferSubDataAsyncExtensionEnabled(webGLVersion) {
+    if (webGLVersion !== 2) {
+        return false;
+    }
+    var gl = getWebGLRenderingContext(webGLVersion);
+    var ext = gl.getExtension('WEBGL_get_buffer_sub_data_async');
+    var isEnabled = ext != null;
+    loseContext(gl);
+    return isEnabled;
+}
+var Environment = (function () {
+    function Environment(features) {
+        this.features = {};
+        this.globalMath = null;
+        this.backendRegistry = {};
+        this.prevBackendRegistry = this.backendRegistry;
+        if (features != null) {
+            this.features = features;
+        }
+    }
+    Environment.prototype.get = function (feature) {
+        if (feature in this.features) {
+            return this.features[feature];
+        }
+        this.features[feature] = this.evaluateFeature(feature);
+        return this.features[feature];
+    };
+    Environment.prototype.getBestBackend = function () {
+        var orderedBackends = ['webgl', 'cpu'];
+        for (var i = 0; i < orderedBackends.length; ++i) {
+            var backendId = orderedBackends[i];
+            if (backendId in this.backendRegistry) {
+                return this.backendRegistry[backendId];
+            }
+        }
+        throw new Error('No backend found in registry.');
+    };
+    Environment.prototype.evaluateFeature = function (feature) {
+        if (feature === 'WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_ENABLED') {
+            var webGLVersion = this.get('WEBGL_VERSION');
+            if (webGLVersion === 0) {
+                return false;
+            }
+            return isWebGLDisjointQueryTimerEnabled(webGLVersion);
+        }
+        else if (feature === 'WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_RELIABLE') {
+            return this.get('WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_ENABLED') &&
+                !device_util.isMobile();
+        }
+        else if (feature === 'WEBGL_VERSION') {
+            if (isWebGLVersionEnabled(2)) {
+                return 2;
+            }
+            else if (isWebGLVersionEnabled(1)) {
+                return 1;
+            }
+            return 0;
+        }
+        else if (feature === 'WEBGL_FLOAT_TEXTURE_ENABLED') {
+            return isFloatTextureReadPixelsEnabled(this.get('WEBGL_VERSION'));
+        }
+        else if (feature === 'WEBGL_GET_BUFFER_SUB_DATA_ASYNC_EXTENSION_ENABLED') {
+            return isWebGLGetBufferSubDataAsyncExtensionEnabled(this.get('WEBGL_VERSION'));
+        }
+        throw new Error("Unknown feature " + feature + ".");
+    };
+    Environment.prototype.setFeatures = function (features) {
+        this.empty();
+        this.features = features;
+    };
+    Environment.prototype.reset = function () {
+        this.globalMath = null;
+        this.backendRegistry = this.prevBackendRegistry;
+        this.features = getFeaturesFromURL();
+    };
+    Environment.prototype.setMath = function (math) {
+        this.globalMath = math;
+    };
+    Environment.prototype.getBackend = function (name) {
+        return this.backendRegistry[name];
+    };
+    Environment.prototype.registerBackend = function (name, factory) {
+        if (name in this.backendRegistry) {
+            throw new Error(name + " backend was already registered");
+        }
+        try {
+            var backend = factory();
+            this.backendRegistry[name] = backend;
+            return true;
+        }
+        catch (err) {
+            return false;
+        }
+    };
+    Object.defineProperty(Environment.prototype, "math", {
+        get: function () {
+            if (this.globalMath == null) {
+                var bestBackend = this.getBestBackend();
+                var safeMode = false;
+                this.globalMath = new math_1.NDArrayMath(bestBackend, safeMode);
+            }
+            return this.globalMath;
+        },
+        enumerable: true,
+        configurable: true
+    });
+    Environment.prototype.empty = function () {
+        this.globalMath = null;
+        this.prevBackendRegistry = this.backendRegistry;
+        this.backendRegistry = {};
+        this.features = null;
+    };
+    return Environment;
+}());
+exports.Environment = Environment;
+var DEEPLEARNJS_FLAGS_PREFIX = 'dljsflags';
+function getFeaturesFromURL() {
+    var features = {};
+    if (typeof window === 'undefined') {
+        return features;
+    }
+    var urlParams = util.getQueryParams(window.location.search);
+    if (DEEPLEARNJS_FLAGS_PREFIX in urlParams) {
+        var urlFlags_1 = {};
+        var keyValues = urlParams[DEEPLEARNJS_FLAGS_PREFIX].split(',');
+        keyValues.forEach(function (keyValue) {
+            var _a = keyValue.split(':'), key = _a[0], value = _a[1];
+            urlFlags_1[key] = value;
+        });
+        exports.URL_PROPERTIES.forEach(function (urlProperty) {
+            if (urlProperty.name in urlFlags_1) {
+                console.log("Setting feature override from URL " + urlProperty.name + ": " +
+                    ("" + urlFlags_1[urlProperty.name]));
+                if (urlProperty.type === Type.NUMBER) {
+                    features[urlProperty.name] = +urlFlags_1[urlProperty.name];
+                }
+                else if (urlProperty.type === Type.BOOLEAN) {
+                    features[urlProperty.name] = urlFlags_1[urlProperty.name] === 'true';
+                }
+                else {
+                    console.warn("Unknown URL param: " + urlProperty.name + ".");
+                }
+            }
+        });
+    }
+    return features;
+}
+function getGlobalNamespace() {
+    var ns;
+    if (typeof (window) !== 'undefined') {
+        ns = window;
+    }
+    else if (typeof (global) !== 'undefined') {
+        ns = global;
+    }
+    else {
+        throw new Error('Could not find a global object');
+    }
+    return ns;
+}
+function getOrMakeEnvironment() {
+    var ns = getGlobalNamespace();
+    ns.ENV = ns.ENV || new Environment(getFeaturesFromURL());
+    return ns.ENV;
+}
+exports.ENV = getOrMakeEnvironment();
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+},{"./device_util":14,"./math/math":94,"./util":101}],16:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var initializers_1 = require("../initializers");
+var concat_util = require("../math/concat_util");
+var conv_util = require("../math/conv_util");
+var ndarray_1 = require("../math/ndarray");
+var util = require("../util");
+var GraphLayers = (function () {
+    function GraphLayers(g) {
+        this.g = g;
+    }
+    GraphLayers.prototype.dense = function (name, x, units, activation, useBias, kernelInitializer, biasInitializer) {
+        if (activation === void 0) { activation = null; }
+        if (useBias === void 0) { useBias = true; }
+        if (kernelInitializer === void 0) { kernelInitializer = new initializers_1.VarianceScalingInitializer(); }
+        if (biasInitializer === void 0) { biasInitializer = new initializers_1.ZerosInitializer(); }
+        var weights = this.g.variable(name + '-weights', kernelInitializer.initialize([x.shape[0], units], x.shape[0], units));
+        var out = this.g.matmul(x, weights);
+        if (useBias) {
+            var bias = this.g.variable(name + '-bias', biasInitializer.initialize([units], x.shape[0], units));
+            out = this.g.add(out, bias);
+        }
+        if (activation != null) {
+            out = activation(out);
+        }
+        return out;
+    };
+    return GraphLayers;
+}());
+exports.GraphLayers = GraphLayers;
+var Graph = (function () {
+    function Graph() {
+        this.nodes = [];
+        this.layers = new GraphLayers(this);
+    }
+    Graph.prototype.variable = function (name, data) {
+        return this.addNodeAndReturnOutput(new VariableNode(this, name, data));
+    };
+    Graph.prototype.placeholder = function (name, shape) {
+        return this.addNodeAndReturnOutput(new PlaceholderNode(this, name, shape));
+    };
+    Graph.prototype.constant = function (value) {
+        var finalValue;
+        if (typeof value === 'number') {
+            finalValue = ndarray_1.Scalar.new(value);
+        }
+        else if (value instanceof ndarray_1.NDArray) {
+            finalValue = value;
+        }
+        else if (value instanceof Array) {
+            var flatValues = util.flatten(value);
+            var vals = new Float32Array(flatValues);
+            finalValue = ndarray_1.NDArray.make(util.inferShape(value), { values: vals });
+        }
+        else {
+            throw new Error('unimplemented constant type.');
+        }
+        return this.addNodeAndReturnOutput(new ConstantNode(this, finalValue));
+    };
+    Graph.prototype.reshape = function (x, shape) {
+        return this.addNodeAndReturnOutput(new ReshapeNode(this, 'Reshape', x, shape));
+    };
+    Graph.prototype.fusedLinearCombination = function (x1, x2, c1, c2) {
+        return this.addNodeAndReturnOutput(new FusedLinearCombinationNode(this, x1, x2, c1, c2));
+    };
+    Graph.prototype.add = function (x1, x2) {
+        return this.addNodeAndReturnOutput(new AddNode(this, x1, x2));
+    };
+    Graph.prototype.subtract = function (x1, x2) {
+        return this.addNodeAndReturnOutput(new SubtractNode(this, x1, x2));
+    };
+    Graph.prototype.multiply = function (x1, x2) {
+        return this.addNodeAndReturnOutput(new MultiplyNode(this, x1, x2));
+    };
+    Graph.prototype.divide = function (x1, x2) {
+        return this.addNodeAndReturnOutput(new DivideNode(this, x1, x2));
+    };
+    Graph.prototype.reduceSum = function (x) {
+        return this.addNodeAndReturnOutput(new ReduceSumNode(this, x));
+    };
+    Graph.prototype.concat3d = function (x1, x2, axis) {
+        return this.addNodeAndReturnOutput(new Concat3DNode(this, x1, x2, axis));
+    };
+    Graph.prototype.matmul = function (x1, x2) {
+        return this.addNodeAndReturnOutput(new MatMulNode(this, x1, x2));
+    };
+    Graph.prototype.conv2d = function (x, w, b, fieldSize, outputDepth, stride, zeroPad) {
+        if (stride === void 0) { stride = 1; }
+        return this.addNodeAndReturnOutput(new Convolution2DNode(this, x, w, b, fieldSize, outputDepth, stride, zeroPad));
+    };
+    Graph.prototype.maxPool = function (x, fieldSize, stride, zeroPad) {
+        if (stride === void 0) { stride = 1; }
+        return this.addNodeAndReturnOutput(new MaxPoolNode(this, x, fieldSize, stride, zeroPad));
+    };
+    Graph.prototype.exp = function (x) {
+        return this.addNodeAndReturnOutput(new ExpNode(this, x));
+    };
+    Graph.prototype.log = function (x) {
+        return this.addNodeAndReturnOutput(new LogNode(this, x));
+    };
+    Graph.prototype.relu = function (x) {
+        return this.addNodeAndReturnOutput(new ReLUNode(this, x));
+    };
+    Graph.prototype.leakyRelu = function (x, alpha) {
+        return this.addNodeAndReturnOutput(new LeakyReLUNode(this, x, alpha));
+    };
+    Graph.prototype.prelu = function (x, alpha) {
+        return this.addNodeAndReturnOutput(new PReLUNode(this, x, alpha));
+    };
+    Graph.prototype.elu = function (x) {
+        return this.addNodeAndReturnOutput(new EluNode(this, x));
+    };
+    Graph.prototype.tanh = function (x) {
+        return this.addNodeAndReturnOutput(new TanHNode(this, x));
+    };
+    Graph.prototype.sigmoid = function (x) {
+        return this.addNodeAndReturnOutput(new SigmoidNode(this, x));
+    };
+    Graph.prototype.square = function (x) {
+        return this.addNodeAndReturnOutput(new SquareNode(this, x));
+    };
+    Graph.prototype.softmax = function (x) {
+        return this.addNodeAndReturnOutput(new SoftmaxNode(this, x));
+    };
+    Graph.prototype.softmaxCrossEntropyCost = function (x, target) {
+        return this.addNodeAndReturnOutput(new SoftmaxCrossEntropyCostNode(this, x, target));
+    };
+    Graph.prototype.meanSquaredCost = function (label, prediction) {
+        return this.addNodeAndReturnOutput(new MeanSquaredCostNode(this, label, prediction));
+    };
+    Graph.prototype.argmax = function (x) {
+        return this.addNodeAndReturnOutput(new ArgMaxNode(this, x));
+    };
+    Graph.prototype.argmaxEquals = function (x1, x2) {
+        return this.addNodeAndReturnOutput(new ArgMaxEqualsNode(this, x1, x2));
+    };
+    Graph.prototype.addNodeAndReturnOutput = function (node) {
+        this.nodes.push(node);
+        node.validate();
+        return node.output;
+    };
+    Graph.prototype.getNodes = function () {
+        return this.nodes;
+    };
+    return Graph;
+}());
+exports.Graph = Graph;
+var Tensor = (function () {
+    function Tensor(shape) {
+        this.shape = shape;
+        this.id = Tensor.nextID++;
+    }
+    Tensor.nextID = 0;
+    return Tensor;
+}());
+exports.Tensor = Tensor;
+var Node = (function () {
+    function Node(graph, name, inputs, output) {
+        this.graph = graph;
+        this.name = name;
+        this.inputs = inputs;
+        this.output = output;
+        this.id = Node.nextID++;
+        output.node = this;
+    }
+    Node.nextID = 0;
+    return Node;
+}());
+exports.Node = Node;
+var VariableNode = (function (_super) {
+    __extends(VariableNode, _super);
+    function VariableNode(graph, name, data) {
+        var _this = _super.call(this, graph, name, {}, new Tensor(data.shape)) || this;
+        _this.data = data;
+        return _this;
+    }
+    VariableNode.prototype.validate = function () {
+        util.assert(this.data != null, 'Error adding variable op: Data for variable \'' + this.name +
+            '\' is null or undefined');
+    };
+    return VariableNode;
+}(Node));
+exports.VariableNode = VariableNode;
+var PlaceholderNode = (function (_super) {
+    __extends(PlaceholderNode, _super);
+    function PlaceholderNode(graph, name, shape) {
+        return _super.call(this, graph, name, {}, new Tensor(shape)) || this;
+    }
+    PlaceholderNode.prototype.validate = function () { };
+    return PlaceholderNode;
+}(Node));
+exports.PlaceholderNode = PlaceholderNode;
+var ConstantNode = (function (_super) {
+    __extends(ConstantNode, _super);
+    function ConstantNode(graph, data) {
+        var _this = _super.call(this, graph, 'Constant', {}, new Tensor(data.shape)) || this;
+        _this.data = data;
+        return _this;
+    }
+    ConstantNode.prototype.validate = function () {
+        util.assert(this.data != null, 'Error adding constant: data for placeholder \'' + this.name +
+            '\' is null or undefined');
+    };
+    return ConstantNode;
+}(Node));
+exports.ConstantNode = ConstantNode;
+var ReshapeNode = (function (_super) {
+    __extends(ReshapeNode, _super);
+    function ReshapeNode(graph, name, x, shape) {
+        var _this = _super.call(this, graph, name, { x: x }, new Tensor(shape)) || this;
+        _this.name = name;
+        _this.x = x;
+        _this.shape = shape;
+        return _this;
+    }
+    ReshapeNode.prototype.validate = function () {
+        var xSize = util.sizeFromShape(this.x.shape);
+        var shapeSize = util.sizeFromShape(this.shape);
+        util.assert(xSize === shapeSize, "Error making reshape operation: input to reshape '" + this.name + "'" +
+            (" of shape (" + this.x.shape + ") does not match size of ") +
+            ("requested shape " + this.shape + "."));
+    };
+    ReshapeNode.X = 'x';
+    return ReshapeNode;
+}(Node));
+exports.ReshapeNode = ReshapeNode;
+var FusedLinearCombinationNode = (function (_super) {
+    __extends(FusedLinearCombinationNode, _super);
+    function FusedLinearCombinationNode(graph, t1, t2, c1, c2) {
+        var _this = _super.call(this, graph, 'Linear Combination', { t1: t1, t2: t2, c1: c1, c2: c2 }, new Tensor(t1.shape)) || this;
+        _this.t1 = t1;
+        _this.t2 = t2;
+        _this.c1 = c1;
+        _this.c2 = c2;
+        return _this;
+    }
+    FusedLinearCombinationNode.prototype.validate = function () {
+        util.assertShapesMatch(this.t1.shape, this.t2.shape);
+        if (!util.isScalarShape(this.c1.shape)) {
+            throw new Error('Error adding fusedLinearCombination: c1 is not a scalar, got ' +
+                ("shape: " + this.c1.shape));
+        }
+        if (!util.isScalarShape(this.c2.shape)) {
+            throw new Error('Error adding fusedLinearCombination: c2 is not a scalar, got ' +
+                ("shape: " + this.c2.shape));
+        }
+    };
+    FusedLinearCombinationNode.T1 = 't1';
+    FusedLinearCombinationNode.T2 = 't2';
+    FusedLinearCombinationNode.C1 = 'c1';
+    FusedLinearCombinationNode.C2 = 'c2';
+    return FusedLinearCombinationNode;
+}(Node));
+exports.FusedLinearCombinationNode = FusedLinearCombinationNode;
+var AddNode = (function (_super) {
+    __extends(AddNode, _super);
+    function AddNode(graph, t1, t2) {
+        var _this = _super.call(this, graph, 'Add', { t1: t1, t2: t2 }, new Tensor(util.sizeFromShape(t1.shape) === 1
+            ? t2.shape
+            : (t1.shape.length < t2.shape.length ? t2.shape : t1.shape))) || this;
+        _this.t1 = t1;
+        _this.t2 = t2;
+        return _this;
+    }
+    AddNode.prototype.validate = function () {
+        util.assert(util.sizeFromShape(this.t1.shape) === 1 ||
+            util.sizeFromShape(this.t2.shape) === 1 ||
+            util.arraysEqual(this.t1.shape, this.t2.shape) ||
+            (this.t1.shape.length === 2 && this.t2.shape.length === 1 &&
+                this.t1.shape[1] === this.t2.shape[0]) ||
+            (this.t1.shape.length === 1 && this.t2.shape.length === 2 &&
+                this.t1.shape[0] === this.t2.shape[1]), 'Error adding add operation op: one of inputs must be scalar, ' +
+            ("shapes " + this.t1.shape + " and " + this.t2.shape + " must match,") +
+            'or one of them can be broadcasted (2D and 1D).');
+    };
+    AddNode.T1 = 't1';
+    AddNode.T2 = 't2';
+    return AddNode;
+}(Node));
+exports.AddNode = AddNode;
+var SubtractNode = (function (_super) {
+    __extends(SubtractNode, _super);
+    function SubtractNode(graph, t1, t2) {
+        var _this = _super.call(this, graph, 'Subtract', { t1: t1, t2: t2 }, new Tensor(util.sizeFromShape(t1.shape) === 1 ? t2.shape : t1.shape)) || this;
+        _this.t1 = t1;
+        _this.t2 = t2;
+        return _this;
+    }
+    SubtractNode.prototype.validate = function () {
+        util.assert(util.sizeFromShape(this.t1.shape) === 1 ||
+            util.sizeFromShape(this.t2.shape) === 1 ||
+            util.arraysEqual(this.t1.shape, this.t2.shape), 'Error adding subtract op: one of inputs must be scalar or the ' +
+            ("shapes " + this.t1.shape + " and " + this.t2.shape + " must match."));
+    };
+    SubtractNode.T1 = 't1';
+    SubtractNode.T2 = 't2';
+    return SubtractNode;
+}(Node));
+exports.SubtractNode = SubtractNode;
+var MultiplyNode = (function (_super) {
+    __extends(MultiplyNode, _super);
+    function MultiplyNode(graph, t1, t2) {
+        var _this = _super.call(this, graph, 'Multiply', { t1: t1, t2: t2 }, new Tensor(util.sizeFromShape(t1.shape) === 1 ? t2.shape : t1.shape)) || this;
+        _this.t1 = t1;
+        _this.t2 = t2;
+        return _this;
+    }
+    MultiplyNode.prototype.validate = function () {
+        util.assert(util.sizeFromShape(this.t1.shape) === 1 ||
+            util.sizeFromShape(this.t2.shape) === 1 ||
+            util.arraysEqual(this.t1.shape, this.t2.shape), 'Error adding multiply op: one of inputs must be scalar or the ' +
+            ("shapes " + this.t1.shape + " and " + this.t2.shape + " must match."));
+    };
+    MultiplyNode.T1 = 't1';
+    MultiplyNode.T2 = 't2';
+    return MultiplyNode;
+}(Node));
+exports.MultiplyNode = MultiplyNode;
+var DivideNode = (function (_super) {
+    __extends(DivideNode, _super);
+    function DivideNode(graph, t1, t2) {
+        var _this = _super.call(this, graph, 'Divide', { t1: t1, t2: t2 }, new Tensor(util.sizeFromShape(t1.shape) === 1 ? t2.shape : t1.shape)) || this;
+        _this.t1 = t1;
+        _this.t2 = t2;
+        return _this;
+    }
+    DivideNode.prototype.validate = function () {
+        util.assert(util.sizeFromShape(this.t1.shape) === 1 ||
+            util.sizeFromShape(this.t2.shape) === 1 ||
+            util.arraysEqual(this.t1.shape, this.t2.shape), 'Error adding divide op: one of inputs must be scalar or the ' +
+            ("shapes " + this.t1.shape + " and " + this.t2.shape + " must match."));
+    };
+    DivideNode.T1 = 't1';
+    DivideNode.T2 = 't2';
+    return DivideNode;
+}(Node));
+exports.DivideNode = DivideNode;
+var ReduceSumNode = (function (_super) {
+    __extends(ReduceSumNode, _super);
+    function ReduceSumNode(graph, x) {
+        return _super.call(this, graph, 'ReduceSum', { x: x }, new Tensor([])) || this;
+    }
+    ReduceSumNode.prototype.validate = function () { };
+    ReduceSumNode.X = 'x';
+    return ReduceSumNode;
+}(Node));
+exports.ReduceSumNode = ReduceSumNode;
+var Concat3DNode = (function (_super) {
+    __extends(Concat3DNode, _super);
+    function Concat3DNode(graph, x1, x2, axis) {
+        var _this = _super.call(this, graph, 'Concat3D', { x1: x1, x2: x2 }, new Tensor(concat_util.computeOutShape(x1.shape, x2.shape, axis))) || this;
+        _this.x1 = x1;
+        _this.x2 = x2;
+        _this.axis = axis;
+        return _this;
+    }
+    Concat3DNode.prototype.validate = function () {
+        concat_util.assertParams(this.x1.shape, this.x2.shape, this.axis);
+    };
+    Concat3DNode.X1 = 'x1';
+    Concat3DNode.X2 = 'x2';
+    Concat3DNode.AXIS = 'axis';
+    return Concat3DNode;
+}(Node));
+exports.Concat3DNode = Concat3DNode;
+function getMatMulOutputShape(x1Shape, x2Shape) {
+    if (x1Shape.length === 1 && x2Shape.length === 1) {
+        return [1];
+    }
+    else if (x1Shape.length === 1 && x2Shape.length === 2) {
+        return [x2Shape[1]];
+    }
+    else if (x1Shape.length === 2 && x2Shape.length === 1) {
+        return [x1Shape[0]];
+    }
+    return [x1Shape[0], x2Shape[1]];
+}
+var MatMulNode = (function (_super) {
+    __extends(MatMulNode, _super);
+    function MatMulNode(graph, x1, x2) {
+        var _this = _super.call(this, graph, 'MatMul', { x1: x1, x2: x2 }, new Tensor(getMatMulOutputShape(x1.shape, x2.shape))) || this;
+        _this.x1 = x1;
+        _this.x2 = x2;
+        return _this;
+    }
+    MatMulNode.prototype.validate = function () {
+        if (this.x1.shape.length === 2 && this.x2.shape.length === 2) {
+            util.assert(this.x1.shape[1] === this.x2.shape[0], 'Error adding matmul op: inner shapes of matrices with shapes ' +
+                (this.x1.shape + " and " + this.x2.shape + " must match."));
+        }
+        else if (this.x1.shape.length === 2 && this.x2.shape.length === 1) {
+            util.assert(this.x1.shape[1] === this.x2.shape[0], 'Error adding matmul op: second dimension of matrix with shape ' +
+                this.x1.shape.toString() +
+                (" must match size of vector with shape " + this.x2.shape + "."));
+        }
+        else if (this.x1.shape.length === 1 && this.x2.shape.length === 2) {
+            util.assert(this.x1.shape[0] === this.x2.shape[0], "Error adding matmul op: size of vector with shape " + this.x1.shape +
+                " must match first dimension of matrix with " +
+                ("shape " + this.x2.shape + "."));
+        }
+        else {
+            throw new Error('Error adding matmul op: inputs must be vectors or matrices.');
+        }
+    };
+    MatMulNode.X1 = 'x1';
+    MatMulNode.X2 = 'x2';
+    return MatMulNode;
+}(Node));
+exports.MatMulNode = MatMulNode;
+var Convolution2DNode = (function (_super) {
+    __extends(Convolution2DNode, _super);
+    function Convolution2DNode(graph, x, w, b, fieldSize, outputDepth, stride, zeroPad) {
+        if (stride === void 0) { stride = 1; }
+        var _this = _super.call(this, graph, 'Convolution 2D', { x: x, w: w, b: b }, new Tensor(conv_util.computeOutputShape3D(x.shape, fieldSize, outputDepth, stride, zeroPad))) || this;
+        _this.x = x;
+        _this.w = w;
+        _this.b = b;
+        _this.fieldSize = fieldSize;
+        _this.outputDepth = outputDepth;
+        _this.stride = stride;
+        _this.zeroPad = zeroPad;
+        return _this;
+    }
+    Convolution2DNode.prototype.validate = function () {
+        util.assert(this.x.shape.length === 3, 'Error adding conv2d op: input must be of rank 3, but got shape: ' +
+            (this.x.shape + "."));
+        util.assert(this.w.shape.length === 4, 'Error adding conv2d op: weights must be of rank 4, but got shape: ' +
+            (this.w.shape + "."));
+        util.assert(this.b.shape.length === 1, 'Error adding conv2d op: biases must be of rank 1, but got shape: ' +
+            (this.b.shape + "."));
+        util.assert(this.x.shape[2] === this.w.shape[2], "Error adding conv2d op: depth of input (" + this.x.shape[2] + ") " +
+            ("must match input depth for weights (" + this.w.shape[2] + ")."));
+    };
+    Convolution2DNode.X = 'x';
+    Convolution2DNode.W = 'w';
+    Convolution2DNode.B = 'b';
+    return Convolution2DNode;
+}(Node));
+exports.Convolution2DNode = Convolution2DNode;
+var MaxPoolNode = (function (_super) {
+    __extends(MaxPoolNode, _super);
+    function MaxPoolNode(graph, x, fieldSize, stride, zeroPad) {
+        if (stride === void 0) { stride = 1; }
+        var _this = _super.call(this, graph, 'Max pool', { x: x }, new Tensor(conv_util.computeOutputShape3D(x.shape, fieldSize, x.shape[2], stride, zeroPad))) || this;
+        _this.x = x;
+        _this.fieldSize = fieldSize;
+        _this.stride = stride;
+        _this.zeroPad = zeroPad;
+        return _this;
+    }
+    MaxPoolNode.prototype.validate = function () {
+        util.assert(this.x.shape.length === 3, 'Error adding maxPool op: input must be of rank 3, but got shape: ' +
+            (this.x.shape + "."));
+    };
+    MaxPoolNode.X = 'x';
+    return MaxPoolNode;
+}(Node));
+exports.MaxPoolNode = MaxPoolNode;
+var ReLUNode = (function (_super) {
+    __extends(ReLUNode, _super);
+    function ReLUNode(graph, x) {
+        return _super.call(this, graph, 'ReLU', { x: x }, new Tensor(x.shape)) || this;
+    }
+    ReLUNode.prototype.validate = function () { };
+    ReLUNode.X = 'x';
+    return ReLUNode;
+}(Node));
+exports.ReLUNode = ReLUNode;
+var LeakyReLUNode = (function (_super) {
+    __extends(LeakyReLUNode, _super);
+    function LeakyReLUNode(graph, x, alpha) {
+        var _this = _super.call(this, graph, 'LeakyReLU', { x: x }, new Tensor(x.shape)) || this;
+        _this.alpha = alpha;
+        return _this;
+    }
+    LeakyReLUNode.prototype.validate = function () { };
+    LeakyReLUNode.X = 'x';
+    return LeakyReLUNode;
+}(Node));
+exports.LeakyReLUNode = LeakyReLUNode;
+var PReLUNode = (function (_super) {
+    __extends(PReLUNode, _super);
+    function PReLUNode(graph, x, alpha) {
+        var _this = _super.call(this, graph, 'PReLU', { x: x, alpha: alpha }, new Tensor(x.shape)) || this;
+        _this.x = x;
+        _this.alpha = alpha;
+        return _this;
+    }
+    PReLUNode.prototype.validate = function () {
+        util.assert(util.arraysEqual(this.x.shape, this.alpha.shape), 'Error adding pRelu op: the ' +
+            ("shapes x: " + this.x.shape + " and alpha: " + this.alpha.shape + " must match."));
+    };
+    PReLUNode.X = 'x';
+    PReLUNode.ALPHA = 'alpha';
+    return PReLUNode;
+}(Node));
+exports.PReLUNode = PReLUNode;
+var EluNode = (function (_super) {
+    __extends(EluNode, _super);
+    function EluNode(graph, x) {
+        return _super.call(this, graph, 'Elu', { x: x }, new Tensor(x.shape)) || this;
+    }
+    EluNode.prototype.validate = function () { };
+    EluNode.X = 'x';
+    return EluNode;
+}(Node));
+exports.EluNode = EluNode;
+var ExpNode = (function (_super) {
+    __extends(ExpNode, _super);
+    function ExpNode(graph, x) {
+        return _super.call(this, graph, 'Exp', { x: x }, new Tensor(x.shape)) || this;
+    }
+    ExpNode.prototype.validate = function () { };
+    ExpNode.X = 'x';
+    return ExpNode;
+}(Node));
+exports.ExpNode = ExpNode;
+var LogNode = (function (_super) {
+    __extends(LogNode, _super);
+    function LogNode(graph, x) {
+        return _super.call(this, graph, 'Log', { x: x }, new Tensor(x.shape)) || this;
+    }
+    LogNode.prototype.validate = function () { };
+    LogNode.X = 'x';
+    return LogNode;
+}(Node));
+exports.LogNode = LogNode;
+var TanHNode = (function (_super) {
+    __extends(TanHNode, _super);
+    function TanHNode(graph, x) {
+        return _super.call(this, graph, 'TanH', { x: x }, new Tensor(x.shape)) || this;
+    }
+    TanHNode.prototype.validate = function () { };
+    TanHNode.X = 'x';
+    return TanHNode;
+}(Node));
+exports.TanHNode = TanHNode;
+var SigmoidNode = (function (_super) {
+    __extends(SigmoidNode, _super);
+    function SigmoidNode(graph, x) {
+        return _super.call(this, graph, 'Sigmoid', { x: x }, new Tensor(x.shape)) || this;
+    }
+    SigmoidNode.prototype.validate = function () { };
+    SigmoidNode.X = 'x';
+    return SigmoidNode;
+}(Node));
+exports.SigmoidNode = SigmoidNode;
+var SquareNode = (function (_super) {
+    __extends(SquareNode, _super);
+    function SquareNode(graph, x) {
+        return _super.call(this, graph, 'Square', { x: x }, new Tensor(x.shape)) || this;
+    }
+    SquareNode.prototype.validate = function () { };
+    SquareNode.X = 'x';
+    return SquareNode;
+}(Node));
+exports.SquareNode = SquareNode;
+var SoftmaxCrossEntropyCostNode = (function (_super) {
+    __extends(SoftmaxCrossEntropyCostNode, _super);
+    function SoftmaxCrossEntropyCostNode(graph, x, target) {
+        var _this = _super.call(this, graph, 'SoftmaxCrossEntropyCost', { x: x, target: target }, new Tensor([])) || this;
+        _this.x = x;
+        _this.target = target;
+        return _this;
+    }
+    SoftmaxCrossEntropyCostNode.prototype.validate = function () {
+        util.assert(util.arraysEqual(this.x.shape, this.target.shape), "Error adding softmaxCrossEntropyCost op: x shape (" + this.x.shape + ") " +
+            ("must match target shape (" + this.target.shape + ")."));
+    };
+    SoftmaxCrossEntropyCostNode.X = 'x';
+    SoftmaxCrossEntropyCostNode.TARGET = 'target';
+    return SoftmaxCrossEntropyCostNode;
+}(Node));
+exports.SoftmaxCrossEntropyCostNode = SoftmaxCrossEntropyCostNode;
+var SoftmaxNode = (function (_super) {
+    __extends(SoftmaxNode, _super);
+    function SoftmaxNode(graph, x) {
+        var _this = _super.call(this, graph, 'Softmax', { x: x }, new Tensor(x.shape)) || this;
+        _this.x = x;
+        return _this;
+    }
+    SoftmaxNode.prototype.validate = function () {
+        util.assert(this.x.shape.length === 1, 'The input to a softmax must be a 1-D tensor');
+        util.assert(this.x.shape[0] >= 2, 'The input to a softmax must have at least 2 values');
+    };
+    SoftmaxNode.X = 'x';
+    return SoftmaxNode;
+}(Node));
+exports.SoftmaxNode = SoftmaxNode;
+var MeanSquaredCostNode = (function (_super) {
+    __extends(MeanSquaredCostNode, _super);
+    function MeanSquaredCostNode(graph, label, prediction) {
+        var _this = _super.call(this, graph, 'Mean Squared Cost', { label: label, prediction: prediction }, new Tensor([])) || this;
+        _this.label = label;
+        _this.prediction = prediction;
+        return _this;
+    }
+    MeanSquaredCostNode.prototype.validate = function () {
+        util.assert(util.arraysEqual(this.label.shape, this.prediction.shape), "Error adding meanSquaredCost op: label shape (" + this.label.shape + ") " +
+            ("must match prediction shape (" + this.prediction.shape + ")."));
+    };
+    MeanSquaredCostNode.LABEL = 'label';
+    MeanSquaredCostNode.PREDICTION = 'prediction';
+    return MeanSquaredCostNode;
+}(Node));
+exports.MeanSquaredCostNode = MeanSquaredCostNode;
+var ArgMaxNode = (function (_super) {
+    __extends(ArgMaxNode, _super);
+    function ArgMaxNode(graph, x) {
+        var _this = _super.call(this, graph, 'ArgMax', { x: x }, new Tensor([1])) || this;
+        _this.x = x;
+        return _this;
+    }
+    ArgMaxNode.prototype.validate = function () {
+        util.assert(util.sizeFromShape(this.x.shape) > 0, 'Error adding argmax op: input tensor must have at least one entry.');
+    };
+    ArgMaxNode.X = 'x';
+    return ArgMaxNode;
+}(Node));
+exports.ArgMaxNode = ArgMaxNode;
+var ArgMaxEqualsNode = (function (_super) {
+    __extends(ArgMaxEqualsNode, _super);
+    function ArgMaxEqualsNode(graph, x1, x2) {
+        var _this = _super.call(this, graph, 'ArgMaxEquals', { x1: x1, x2: x2 }, new Tensor([1])) || this;
+        _this.x1 = x1;
+        _this.x2 = x2;
+        return _this;
+    }
+    ArgMaxEqualsNode.prototype.validate = function () {
+        util.assert(util.arraysEqual(this.x1.shape, this.x2.shape), "Error adding ArgMaxEquals op: x1 shape (" + this.x1.shape + ") " +
+            ("must match x2 shape (" + this.x2.shape + ")."));
+    };
+    ArgMaxEqualsNode.X1 = 'x1';
+    ArgMaxEqualsNode.X2 = 'x2';
+    return ArgMaxEqualsNode;
+}(Node));
+exports.ArgMaxEqualsNode = ArgMaxEqualsNode;
+
+},{"../initializers":52,"../math/concat_util":91,"../math/conv_util":92,"../math/ndarray":95,"../util":101}],17:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var graph_1 = require("./graph");
+var priority_queue = require("./priority_queue");
+var priority_queue_1 = require("./priority_queue");
+function getUnorderedEvaluationSet(nodes, terminatingNodes) {
+    var terminatingNodeMap = {};
+    var seen = {};
+    var set = [];
+    var visit = nodes.slice();
+    terminatingNodes.forEach(function (node) { return terminatingNodeMap[node.id] = node; });
+    var _loop_1 = function () {
+        var cur = visit.pop();
+        if (seen[cur.id] == null) {
+            if (terminatingNodeMap[cur.id] == null) {
+                Object.keys(cur.inputs)
+                    .map(function (inputName) { return cur.inputs[inputName]; })
+                    .forEach(function (input) { return visit.push(input.node); });
+            }
+            set.push(cur);
+            seen[cur.id] = cur;
+        }
+    };
+    while (visit.length !== 0) {
+        _loop_1();
+    }
+    return set;
+}
+exports.getUnorderedEvaluationSet = getUnorderedEvaluationSet;
+function getOrderedEvaluationSet(unorderedEvaluationSet) {
+    var set = [];
+    var nodeIndices = {};
+    var pendingDependencies = {};
+    var nodeQueue = new priority_queue_1.PriorityQueue(function (a, b) { return priority_queue.defaultCompare(pendingDependencies[a.id], pendingDependencies[b.id]); }, function (node, newIndex) { return nodeIndices[node.id] = newIndex; });
+    unorderedEvaluationSet.forEach(function (node) { return pendingDependencies[node.id] = 0; });
+    unorderedEvaluationSet.forEach(function (node) { return Object.keys(node.inputs)
+        .map(function (key) { return node.inputs[key]; })
+        .forEach(function (input) {
+        if (unorderedEvaluationSet.indexOf(input.node) !== -1) {
+            pendingDependencies[input.node.id]++;
+        }
+    }); });
+    unorderedEvaluationSet.forEach(function (node) { return nodeQueue.enqueue(node); });
+    while (!nodeQueue.empty()) {
+        set.unshift(nodeQueue.dequeue());
+        Object.keys(set[0].inputs).map(function (key) { return set[0].inputs[key]; }).forEach(function (input) {
+            if (unorderedEvaluationSet.indexOf(input.node) === -1) {
+                return;
+            }
+            pendingDependencies[input.node.id]--;
+            nodeQueue.update(input.node, nodeIndices[input.node.id]);
+        });
+    }
+    return set;
+}
+exports.getOrderedEvaluationSet = getOrderedEvaluationSet;
+function isInputNode(node) {
+    return Object.keys(node.inputs).length === 0;
+}
+exports.isInputNode = isInputNode;
+function shouldBackProp(t) {
+    return !(t.node instanceof graph_1.ConstantNode);
+}
+exports.shouldBackProp = shouldBackProp;
+function isPassthroughNode(node, map) {
+    var keys = Object.keys(node.inputs);
+    for (var i = 0; i < keys.length; i++) {
+        var input = node.inputs[keys[i]];
+        if (map.get(input, true) === map.get(node.output, true)) {
+            return true;
+        }
+    }
+    return false;
+}
+exports.isPassthroughNode = isPassthroughNode;
+
+},{"./graph":16,"./priority_queue":46}],18:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var graph_1 = require("./graph");
+var graph_util = require("./graph_util");
+var add_1 = require("./ops/add");
+var argmax_1 = require("./ops/argmax");
+var argmaxequals_1 = require("./ops/argmaxequals");
+var concat3d_1 = require("./ops/concat3d");
+var convolution_1 = require("./ops/convolution");
+var divide_1 = require("./ops/divide");
+var element_wise_activation_1 = require("./ops/element_wise_activation");
+var element_wise_cost_1 = require("./ops/element_wise_cost");
+var exp_1 = require("./ops/exp");
+var linear_combination_1 = require("./ops/linear_combination");
+var log_1 = require("./ops/log");
+var matmul_1 = require("./ops/matmul");
+var max_pool_1 = require("./ops/max_pool");
+var multiply_1 = require("./ops/multiply");
+var reduce_sum_1 = require("./ops/reduce_sum");
+var reshape_1 = require("./ops/reshape");
+var softmax_1 = require("./ops/softmax");
+var subtract_1 = require("./ops/subtract");
+function emitFromGraphNodes(nodes) {
+    var ops = [];
+    nodes.forEach(function (node) { return Array.prototype.push.apply(ops, emitOpFromNode(node)); });
+    return ops;
+}
+exports.emitFromGraphNodes = emitFromGraphNodes;
+function emitOpFromNode(node) {
+    if (node instanceof graph_1.ReshapeNode) {
+        return [new reshape_1.Reshape(node.inputs[graph_1.ReshapeNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.MatMulNode) {
+        var x1 = node.inputs[graph_1.MatMulNode.X1];
+        var x2 = node.inputs[graph_1.MatMulNode.X2];
+        return [new matmul_1.MatMul(x1, x2, node.output)];
+    }
+    else if (node instanceof graph_1.Convolution2DNode) {
+        var w = node.inputs[graph_1.Convolution2DNode.W];
+        var x = node.inputs[graph_1.Convolution2DNode.X];
+        var b = node.inputs[graph_1.Convolution2DNode.B];
+        return [new convolution_1.Convolution2D(w, x, b, node.output, node.fieldSize, node.outputDepth, node.stride, node.zeroPad)];
+    }
+    else if (node instanceof graph_1.MaxPoolNode) {
+        var x = node.inputs[graph_1.MaxPoolNode.X];
+        return [new max_pool_1.MaxPool(x, node.output, node.fieldSize, node.stride, node.zeroPad)];
+    }
+    else if (node instanceof graph_1.ExpNode) {
+        return [new exp_1.Exp(node.inputs[graph_1.ExpNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.LogNode) {
+        return [new log_1.Log(node.inputs[graph_1.LogNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.ReLUNode) {
+        return [new element_wise_activation_1.ReLU(node.inputs[graph_1.ReLUNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.LeakyReLUNode) {
+        return [new element_wise_activation_1.LeakyReLU(node.inputs[graph_1.LeakyReLUNode.X], node.output, node.alpha)];
+    }
+    else if (node instanceof graph_1.PReLUNode) {
+        return [new element_wise_activation_1.PReLU(node.inputs[graph_1.PReLUNode.X], node.inputs[graph_1.PReLUNode.ALPHA], node.output)];
+    }
+    else if (node instanceof graph_1.EluNode) {
+        return [new element_wise_activation_1.Elu(node.inputs[graph_1.EluNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.TanHNode) {
+        return [new element_wise_activation_1.TanH(node.inputs[graph_1.TanHNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.SigmoidNode) {
+        return [new element_wise_activation_1.Sigmoid(node.inputs[graph_1.SigmoidNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.SoftmaxCrossEntropyCostNode) {
+        var x = node.inputs[graph_1.SoftmaxCrossEntropyCostNode.X];
+        var target = node.inputs[graph_1.SoftmaxCrossEntropyCostNode.TARGET];
+        return [new softmax_1.SoftmaxCrossEntropyCost(x, target, node.output)];
+    }
+    else if (node instanceof graph_1.SoftmaxNode) {
+        return [new softmax_1.Softmax(node.inputs[graph_1.SoftmaxNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.MeanSquaredCostNode) {
+        var label = node.inputs[graph_1.MeanSquaredCostNode.LABEL];
+        var prediction = node.inputs[graph_1.MeanSquaredCostNode.PREDICTION];
+        return [new element_wise_cost_1.MeanSquaredCost(label, prediction, node.output)];
+    }
+    else if (node instanceof graph_1.ArgMaxEqualsNode) {
+        return [new argmaxequals_1.ArgMaxEquals(node.inputs[graph_1.ArgMaxEqualsNode.X1], node.inputs[graph_1.ArgMaxEqualsNode.X2], node.output)];
+    }
+    else if (node instanceof graph_1.ArgMaxNode) {
+        return [new argmax_1.ArgMax(node.x, node.output)];
+    }
+    else if (node instanceof graph_1.FusedLinearCombinationNode) {
+        return [new linear_combination_1.LinearCombination(node.inputs[graph_1.FusedLinearCombinationNode.T1], node.inputs[graph_1.FusedLinearCombinationNode.T2], node.inputs[graph_1.FusedLinearCombinationNode.C1], node.inputs[graph_1.FusedLinearCombinationNode.C2], node.output)];
+    }
+    else if (node instanceof graph_1.Concat3DNode) {
+        return [new concat3d_1.Concat3D(node.inputs[graph_1.Concat3DNode.X1], node.inputs[graph_1.Concat3DNode.X2], node.axis, node.output)];
+    }
+    else if (node instanceof graph_1.SquareNode) {
+        return [new element_wise_activation_1.Square(node.inputs[graph_1.SquareNode.X], node.output)];
+    }
+    else if (node instanceof graph_1.AddNode) {
+        return [new add_1.Add(node.inputs[graph_1.AddNode.T1], node.inputs[graph_1.AddNode.T2], node.output)];
+    }
+    else if (node instanceof graph_1.SubtractNode) {
+        return [new subtract_1.Subtract(node.inputs[graph_1.SubtractNode.T1], node.inputs[graph_1.SubtractNode.T2], node.output)];
+    }
+    else if (node instanceof graph_1.MultiplyNode) {
+        return [new multiply_1.Multiply(node.inputs[graph_1.MultiplyNode.T1], node.inputs[graph_1.MultiplyNode.T2], node.output)];
+    }
+    else if (node instanceof graph_1.DivideNode) {
+        return [new divide_1.Divide(node.inputs[graph_1.DivideNode.T1], node.inputs[graph_1.DivideNode.T2], node.output)];
+    }
+    else if (node instanceof graph_1.ReduceSumNode) {
+        return [new reduce_sum_1.ReduceSum(node.inputs[graph_1.ReduceSumNode.X], node.output)];
+    }
+    else if (graph_util.isInputNode(node)) {
+        return [];
+    }
+    else {
+        throw Error("Unsupported node type: " + node.constructor.name);
+    }
+}
+
+},{"./graph":16,"./graph_util":17,"./ops/add":19,"./ops/argmax":20,"./ops/argmaxequals":21,"./ops/concat3d":22,"./ops/convolution":23,"./ops/divide":24,"./ops/element_wise_activation":25,"./ops/element_wise_cost":26,"./ops/exp":27,"./ops/linear_combination":28,"./ops/log":29,"./ops/matmul":30,"./ops/max_pool":31,"./ops/multiply":32,"./ops/reduce_sum":34,"./ops/reshape":35,"./ops/softmax":36,"./ops/subtract":37}],19:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../../util");
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var Add = (function (_super) {
+    __extends(Add, _super);
+    function Add(x1Tensor, x2Tensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.x1Tensor = x1Tensor;
+        _this.x2Tensor = x2Tensor;
+        _this.yTensor = yTensor;
+        util.assert(util.sizeFromShape(x1Tensor.shape) === 1 ||
+            util.sizeFromShape(x2Tensor.shape) === 1 ||
+            util.arraysEqual(x1Tensor.shape, x2Tensor.shape) ||
+            (x1Tensor.shape.length === 2 && x2Tensor.shape.length === 1 &&
+                x1Tensor.shape[1] === x2Tensor.shape[0]) ||
+            (x1Tensor.shape.length === 1 && x2Tensor.shape.length === 2 &&
+                x1Tensor.shape[0] === x2Tensor.shape[1]), 'One of t1 or t2 must be a scalar, or t1 and t2 must have ' +
+            'the same shape, ' +
+            'or one of them can be broadcasted (2D and 1D).');
+        return _this;
+    }
+    Add.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        math.scope(function (keep) {
+            var result;
+            if (util.isScalarShape(x1.shape)) {
+                result = math.scalarPlusArray(x1, x2);
+            }
+            else if (util.isScalarShape(x2.shape)) {
+                result = math.scalarPlusArray(x2, x1);
+            }
+            else {
+                result = math.add(x1, x2);
+            }
+            inferenceArrays.set(_this.yTensor, keep(result));
+        });
+    };
+    Add.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var dy = gradientArrays.get(this.yTensor);
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.x1Tensor)) {
+                if (_this.x1Tensor.shape.length === 1 &&
+                    _this.x2Tensor.shape.length === 2 &&
+                    _this.x1Tensor.shape[0] === _this.x2Tensor.shape[1]) {
+                    var sum = math.sum(dy, 0);
+                    gradientArrays.add(_this.x1Tensor, sum);
+                }
+                else if (util.isScalarShape(_this.x1Tensor.shape)) {
+                    var sum = math.sum(dy);
+                    gradientArrays.add(_this.x1Tensor, sum);
+                }
+                else {
+                    gradientArrays.add(_this.x1Tensor, math.clone(dy));
+                }
+            }
+            if (graph_util.shouldBackProp(_this.x2Tensor)) {
+                if (_this.x1Tensor.shape.length === 2 &&
+                    _this.x2Tensor.shape.length === 1 &&
+                    _this.x1Tensor.shape[1] === _this.x2Tensor.shape[0]) {
+                    var sum = math.sum(dy, 0);
+                    gradientArrays.add(_this.x2Tensor, sum);
+                }
+                else if (util.isScalarShape(_this.x2Tensor.shape)) {
+                    var sum = math.sum(dy);
+                    gradientArrays.add(_this.x2Tensor, sum);
+                }
+                else {
+                    gradientArrays.add(_this.x2Tensor, math.clone(dy));
+                }
+            }
+        });
+    };
+    Add.prototype.dispose = function () {
+        if (this.dySizeScalar != null) {
+            this.dySizeScalar.dispose();
+        }
+    };
+    return Add;
+}(op_1.Operation));
+exports.Add = Add;
+
+},{"../../util":101,"../graph_util":17,"./op":33}],20:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var op_1 = require("./op");
+var ArgMax = (function (_super) {
+    __extends(ArgMax, _super);
+    function ArgMax(xTensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.xTensor = xTensor;
+        _this.yTensor = yTensor;
+        return _this;
+    }
+    ArgMax.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(math.argMax(x)));
+        });
+    };
+    ArgMax.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        throw new Error('ArgMax backprop unimplemented');
+    };
+    return ArgMax;
+}(op_1.Operation));
+exports.ArgMax = ArgMax;
+
+},{"./op":33}],21:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var op_1 = require("./op");
+var ArgMaxEquals = (function (_super) {
+    __extends(ArgMaxEquals, _super);
+    function ArgMaxEquals(x1Tensor, x2Tensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.x1Tensor = x1Tensor;
+        _this.x2Tensor = x2Tensor;
+        _this.yTensor = yTensor;
+        return _this;
+    }
+    ArgMaxEquals.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(math.argMaxEquals(x1, x2)));
+        });
+    };
+    ArgMaxEquals.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        throw new Error('ArgMaxEquals backprop unimplemented');
+    };
+    return ArgMaxEquals;
+}(op_1.Operation));
+exports.ArgMaxEquals = ArgMaxEquals;
+
+},{"./op":33}],22:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var concat_util = require("../../math/concat_util");
+var op_1 = require("./op");
+var Concat3D = (function (_super) {
+    __extends(Concat3D, _super);
+    function Concat3D(x1Tensor, x2Tensor, axis, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.x1Tensor = x1Tensor;
+        _this.x2Tensor = x2Tensor;
+        _this.axis = axis;
+        _this.yTensor = yTensor;
+        concat_util.assertParams(x1Tensor.shape, x2Tensor.shape, axis);
+        return _this;
+    }
+    Concat3D.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        math.scope(function (keep) {
+            var concatResult = math.concat3D(x1, x2, _this.axis);
+            inferenceArrays.set(_this.yTensor, keep(concatResult));
+        });
+    };
+    Concat3D.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        throw new Error('Concat3D backprop not implemented.');
+    };
+    return Concat3D;
+}(op_1.Operation));
+exports.Concat3D = Concat3D;
+
+},{"../../math/concat_util":91,"./op":33}],23:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var conv_util = require("../../math/conv_util");
+var util = require("../../util");
+var op_1 = require("./op");
+var Convolution2D = (function (_super) {
+    __extends(Convolution2D, _super);
+    function Convolution2D(wTensor, xTensor, bTensor, yTensor, fieldSize, outputDepth, stride, zeroPad) {
+        if (stride === void 0) { stride = 1; }
+        var _this = _super.call(this) || this;
+        _this.wTensor = wTensor;
+        _this.xTensor = xTensor;
+        _this.bTensor = bTensor;
+        _this.yTensor = yTensor;
+        _this.fieldSize = fieldSize;
+        _this.outputDepth = outputDepth;
+        _this.stride = stride;
+        _this.assertWeightsShape(wTensor.shape);
+        _this.zeroPad = zeroPad != null ?
+            zeroPad :
+            conv_util.computeDefaultPad(_this.xTensor.shape, _this.fieldSize, _this.stride);
+        util.assert(util.isInt(_this.zeroPad), "The zero padding (" + _this.zeroPad + ") must be an integer. Change the " +
+            "stride and/or zero pad parameters");
+        return _this;
+    }
+    Convolution2D.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var weights = inferenceArrays.get(this.wTensor);
+        var biases = inferenceArrays.get(this.bTensor);
+        var x = inferenceArrays.get(this.xTensor);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(math.conv2d(x, weights, biases, _this.stride, _this.zeroPad)));
+        });
+    };
+    Convolution2D.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var filter = inferenceArrays.get(this.wTensor);
+        var x = inferenceArrays.get(this.xTensor);
+        var dy = gradientArrays.get(this.yTensor);
+        math.scope(function () {
+            var dw = math.conv2dDerFilter(x, dy, filter.shape, _this.stride, _this.zeroPad);
+            var db = math.conv2dDerBias(dy);
+            var dx = math.conv2dDerInput(x.shape, dy, filter, _this.stride, _this.zeroPad);
+            gradientArrays.add(_this.wTensor, dw);
+            gradientArrays.add(_this.bTensor, db);
+            gradientArrays.add(_this.xTensor, dx);
+        });
+    };
+    Convolution2D.prototype.assertWeightsShape = function (weightsShape) {
+        util.assert(weightsShape[0] === this.fieldSize &&
+            weightsShape[1] === this.fieldSize &&
+            weightsShape[2] === this.xTensor.shape[2] &&
+            weightsShape[3] === this.outputDepth, "weights must be of shape [" + this.fieldSize + "," + this.fieldSize + "," +
+            (this.xTensor.shape[2] + "," + this.outputDepth + "] but they are of") +
+            ("shape [" + weightsShape + "]"));
+    };
+    return Convolution2D;
+}(op_1.Operation));
+exports.Convolution2D = Convolution2D;
+
+},{"../../math/conv_util":92,"../../util":101,"./op":33}],24:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../../util");
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var Divide = (function (_super) {
+    __extends(Divide, _super);
+    function Divide(x1Tensor, x2Tensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.x1Tensor = x1Tensor;
+        _this.x2Tensor = x2Tensor;
+        _this.yTensor = yTensor;
+        util.assert(util.sizeFromShape(x1Tensor.shape) === 1 ||
+            util.sizeFromShape(x2Tensor.shape) === 1 ||
+            util.arraysEqual(x1Tensor.shape, x2Tensor.shape), 'One of t1 or t2 must be a scalar, or t1 and t2 must have ' +
+            'the same shape');
+        return _this;
+    }
+    Divide.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var t1 = inferenceArrays.get(this.x1Tensor);
+        var t2 = inferenceArrays.get(this.x2Tensor);
+        math.scope(function (keep) {
+            var result;
+            if (util.isScalarShape(t1.shape)) {
+                result = math.scalarDividedByArray(t1, t2);
+            }
+            else if (util.isScalarShape(t2.shape)) {
+                result = math.arrayDividedByScalar(t1, t2);
+            }
+            else {
+                result = math.divide(t1, t2);
+            }
+            inferenceArrays.set(_this.yTensor, keep(result));
+        });
+    };
+    Divide.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        var dy = gradientArrays.get(this.yTensor);
+        var x1IsScalar = util.isScalarShape(x1.shape);
+        var x2IsScalar = util.isScalarShape(x2.shape);
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.x1Tensor)) {
+                if (x1IsScalar) {
+                    var div = math.divide(dy, x2);
+                    gradientArrays.add(_this.x1Tensor, math.sum(div));
+                    div.dispose();
+                }
+                else if (x2IsScalar) {
+                    gradientArrays.add(_this.x1Tensor, math.arrayDividedByScalar(dy, x2));
+                }
+                else {
+                    gradientArrays.add(_this.x1Tensor, math.divide(dy, x2));
+                }
+            }
+            if (graph_util.shouldBackProp(_this.x2Tensor)) {
+                var x2Squared = math.elementWiseMul(x2, x2);
+                var x1OverX2Squared = void 0;
+                if (x2IsScalar) {
+                    x1OverX2Squared = math.arrayDividedByScalar(x1, x2Squared);
+                }
+                else if (x1IsScalar) {
+                    x1OverX2Squared = math.scalarDividedByArray(x1, x2Squared);
+                }
+                else {
+                    x1OverX2Squared = math.divide(x1, x2Squared);
+                }
+                var dx2 = math.neg(x1OverX2Squared);
+                var dyTimesDerivative = math.elementWiseMul(dy, dx2);
+                if (x2IsScalar) {
+                    gradientArrays.add(_this.x2Tensor, math.sum(dyTimesDerivative));
+                }
+                else {
+                    gradientArrays.add(_this.x2Tensor, dyTimesDerivative);
+                }
+            }
+        });
+    };
+    return Divide;
+}(op_1.Operation));
+exports.Divide = Divide;
+
+},{"../../util":101,"../graph_util":17,"./op":33}],25:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var activation_functions_1 = require("../../math/activation_functions");
+var op_1 = require("./op");
+var ElementWiseActivation = (function (_super) {
+    __extends(ElementWiseActivation, _super);
+    function ElementWiseActivation(xTensor, yTensor, func) {
+        var _this = _super.call(this) || this;
+        _this.xTensor = xTensor;
+        _this.yTensor = yTensor;
+        _this.func = func;
+        return _this;
+    }
+    ElementWiseActivation.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(_this.func.output(math, x)));
+        });
+    };
+    ElementWiseActivation.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        var y = inferenceArrays.get(this.yTensor);
+        var dy = gradientArrays.get(this.yTensor);
+        math.scope(function () {
+            var dydx = _this.func.der(math, x, y);
+            gradientArrays.add(_this.xTensor, math.elementWiseMul(dy, dydx));
+            dydx.dispose();
+        });
+    };
+    ElementWiseActivation.prototype.dispose = function () {
+        this.func.dispose();
+    };
+    return ElementWiseActivation;
+}(op_1.Operation));
+exports.ElementWiseActivation = ElementWiseActivation;
+var ReLU = (function (_super) {
+    __extends(ReLU, _super);
+    function ReLU(xTensor, yTensor) {
+        return _super.call(this, xTensor, yTensor, new activation_functions_1.ReLUFunc()) || this;
+    }
+    return ReLU;
+}(ElementWiseActivation));
+exports.ReLU = ReLU;
+var LeakyReLU = (function (_super) {
+    __extends(LeakyReLU, _super);
+    function LeakyReLU(xTensor, yTensor, alpha) {
+        return _super.call(this, xTensor, yTensor, new activation_functions_1.LeakyReluFunc(alpha)) || this;
+    }
+    return LeakyReLU;
+}(ElementWiseActivation));
+exports.LeakyReLU = LeakyReLU;
+var TanH = (function (_super) {
+    __extends(TanH, _super);
+    function TanH(xTensor, yTensor) {
+        return _super.call(this, xTensor, yTensor, new activation_functions_1.TanHFunc()) || this;
+    }
+    return TanH;
+}(ElementWiseActivation));
+exports.TanH = TanH;
+var Sigmoid = (function (_super) {
+    __extends(Sigmoid, _super);
+    function Sigmoid(xTensor, yTensor) {
+        return _super.call(this, xTensor, yTensor, new activation_functions_1.SigmoidFunc()) || this;
+    }
+    return Sigmoid;
+}(ElementWiseActivation));
+exports.Sigmoid = Sigmoid;
+var Square = (function (_super) {
+    __extends(Square, _super);
+    function Square(xTensor, yTensor) {
+        return _super.call(this, xTensor, yTensor, new activation_functions_1.SquareFunc()) || this;
+    }
+    return Square;
+}(ElementWiseActivation));
+exports.Square = Square;
+var Elu = (function (_super) {
+    __extends(Elu, _super);
+    function Elu(xTensor, yTensor) {
+        return _super.call(this, xTensor, yTensor, new activation_functions_1.EluFunc()) || this;
+    }
+    return Elu;
+}(ElementWiseActivation));
+exports.Elu = Elu;
+var PReLU = (function (_super) {
+    __extends(PReLU, _super);
+    function PReLU(xTensor, alphaTensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.xTensor = xTensor;
+        _this.alphaTensor = alphaTensor;
+        _this.yTensor = yTensor;
+        return _this;
+    }
+    PReLU.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        var alpha = inferenceArrays.get(this.alphaTensor);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(math.prelu(x, alpha)));
+        });
+    };
+    PReLU.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        var alpha = inferenceArrays.get(this.alphaTensor);
+        var dy = gradientArrays.get(this.yTensor);
+        math.scope(function () {
+            var dydx = math.preluDer(x, alpha);
+            gradientArrays.add(_this.xTensor, math.elementWiseMul(dy, dydx));
+        });
+    };
+    return PReLU;
+}(op_1.Operation));
+exports.PReLU = PReLU;
+
+},{"../../math/activation_functions":53,"./op":33}],26:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../environment");
+var cost_functions_1 = require("../../math/cost_functions");
+var ndarray_1 = require("../../math/ndarray");
+var util = require("../../util");
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var ElementWiseCost = (function (_super) {
+    __extends(ElementWiseCost, _super);
+    function ElementWiseCost(x1Tensor, x2Tensor, yTensor, func) {
+        var _this = _super.call(this) || this;
+        _this.x1Tensor = x1Tensor;
+        _this.x2Tensor = x2Tensor;
+        _this.yTensor = yTensor;
+        _this.func = func;
+        _this.oneOverNScalar =
+            environment_1.ENV.math.keep(ndarray_1.Scalar.new(1 / util.sizeFromShape(x1Tensor.shape)));
+        return _this;
+    }
+    ElementWiseCost.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        math.scope(function (keep) {
+            var elementWiseCost = _this.func.cost(math, x1, x2);
+            var sum = math.sum(elementWiseCost);
+            var result = math.scalarTimesArray(_this.oneOverNScalar, sum);
+            inferenceArrays.set(_this.yTensor, keep(result));
+        });
+    };
+    ElementWiseCost.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.x1Tensor)) {
+                gradientArrays.add(_this.x1Tensor, _this.func.der(math, x1, x2));
+            }
+            if (graph_util.shouldBackProp(_this.x2Tensor)) {
+                gradientArrays.add(_this.x2Tensor, _this.func.der(math, x2, x1));
+            }
+        });
+    };
+    ElementWiseCost.prototype.dispose = function () {
+        this.func.dispose();
+        this.oneOverNScalar.dispose();
+    };
+    return ElementWiseCost;
+}(op_1.Operation));
+exports.ElementWiseCost = ElementWiseCost;
+var MeanSquaredCost = (function (_super) {
+    __extends(MeanSquaredCost, _super);
+    function MeanSquaredCost(x1Tensor, x2Tensor, yTensor) {
+        return _super.call(this, x1Tensor, x2Tensor, yTensor, new cost_functions_1.SquareCostFunc()) || this;
+    }
+    return MeanSquaredCost;
+}(ElementWiseCost));
+exports.MeanSquaredCost = MeanSquaredCost;
+
+},{"../../environment":15,"../../math/cost_functions":93,"../../math/ndarray":95,"../../util":101,"../graph_util":17,"./op":33}],27:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var Exp = (function (_super) {
+    __extends(Exp, _super);
+    function Exp(xTensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.xTensor = xTensor;
+        _this.yTensor = yTensor;
+        return _this;
+    }
+    Exp.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(math.exp(x)));
+        });
+    };
+    Exp.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var y = inferenceArrays.get(this.yTensor);
+        var dy = gradientArrays.get(this.yTensor);
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.xTensor)) {
+                gradientArrays.add(_this.xTensor, math.elementWiseMul(y, dy));
+            }
+        });
+    };
+    return Exp;
+}(op_1.Operation));
+exports.Exp = Exp;
+
+},{"../graph_util":17,"./op":33}],28:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var LinearCombination = (function (_super) {
+    __extends(LinearCombination, _super);
+    function LinearCombination(x1Tensor, x2Tensor, c1Tensor, c2Tensor, outTensor) {
+        var _this = _super.call(this) || this;
+        _this.x1Tensor = x1Tensor;
+        _this.x2Tensor = x2Tensor;
+        _this.c1Tensor = c1Tensor;
+        _this.c2Tensor = c2Tensor;
+        _this.outTensor = outTensor;
+        return _this;
+    }
+    LinearCombination.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        var c1 = inferenceArrays.get(this.c1Tensor).asScalar();
+        var c2 = inferenceArrays.get(this.c2Tensor).asScalar();
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.outTensor, keep(math.scaledArrayAdd(c1, x1, c2, x2)));
+        });
+    };
+    LinearCombination.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        var c1 = inferenceArrays.get(this.c1Tensor);
+        var c2 = inferenceArrays.get(this.c2Tensor);
+        var dy = gradientArrays.get(this.outTensor);
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.x1Tensor)) {
+                gradientArrays.add(_this.x1Tensor, math.scalarTimesArray(c1, dy));
+            }
+            if (graph_util.shouldBackProp(_this.x2Tensor)) {
+                gradientArrays.add(_this.x2Tensor, math.scalarTimesArray(c2, dy));
+            }
+            if (graph_util.shouldBackProp(_this.c1Tensor)) {
+                var dotProduct1 = math.elementWiseMul(x1, dy);
+                gradientArrays.add(_this.c1Tensor, math.sum(dotProduct1));
+            }
+            if (graph_util.shouldBackProp(_this.c2Tensor)) {
+                var dotProduct2 = math.elementWiseMul(x2, dy);
+                gradientArrays.add(_this.c2Tensor, math.sum(dotProduct2));
+            }
+        });
+    };
+    return LinearCombination;
+}(op_1.Operation));
+exports.LinearCombination = LinearCombination;
+
+},{"../graph_util":17,"./op":33}],29:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var Log = (function (_super) {
+    __extends(Log, _super);
+    function Log(xTensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.xTensor = xTensor;
+        _this.yTensor = yTensor;
+        return _this;
+    }
+    Log.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(math.log(x)));
+        });
+    };
+    Log.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        var dy = gradientArrays.get(this.yTensor);
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.xTensor)) {
+                gradientArrays.add(_this.xTensor, math.divide(dy, x));
+            }
+        });
+    };
+    return Log;
+}(op_1.Operation));
+exports.Log = Log;
+
+},{"../graph_util":17,"./op":33}],30:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var matmul_1 = require("../../math/backends/types/matmul");
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var MatMul = (function (_super) {
+    __extends(MatMul, _super);
+    function MatMul(x1Tensor, x2Tensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.x1Tensor = x1Tensor;
+        _this.x2Tensor = x2Tensor;
+        _this.yTensor = yTensor;
+        return _this;
+    }
+    MatMul.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        math.scope(function (keep) {
+            if (x1.shape.length === 2 && x2.shape.length === 2) {
+                inferenceArrays.set(_this.yTensor, keep(math.matMul(x1, x2)));
+            }
+            else if (x1.shape.length === 2 && x2.shape.length === 1) {
+                inferenceArrays.set(_this.yTensor, keep(math.matrixTimesVector(x1, x2)));
+            }
+            else if (x1.shape.length === 1 && x2.shape.length === 2) {
+                inferenceArrays.set(_this.yTensor, keep(math.vectorTimesMatrix(x1, x2)));
+            }
+        });
+    };
+    MatMul.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        var dy = gradientArrays.get(this.yTensor);
+        if (x1.shape.length === 1) {
+            x1 = x1.reshape([1, x1.size]);
+            dy = dy.reshape([1, dy.size]);
+        }
+        if (x2.shape.length === 1) {
+            x2 = x2.reshape([x2.size, 1]);
+            dy = dy.reshape([dy.size, 1]);
+        }
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.x1Tensor)) {
+                var dx1 = math.matMul(dy, x2, matmul_1.MatrixOrientation.REGULAR, matmul_1.MatrixOrientation.TRANSPOSED);
+                gradientArrays.add(_this.x1Tensor, _this.x1Tensor.shape.length === 1 ? dx1.as1D() : dx1);
+            }
+            if (graph_util.shouldBackProp(_this.x2Tensor)) {
+                var dx2 = math.matMul(x1, dy, matmul_1.MatrixOrientation.TRANSPOSED, matmul_1.MatrixOrientation.REGULAR);
+                gradientArrays.add(_this.x2Tensor, _this.x2Tensor.shape.length === 1 ? dx2.as1D() : dx2);
+            }
+        });
+    };
+    return MatMul;
+}(op_1.Operation));
+exports.MatMul = MatMul;
+
+},{"../../math/backends/types/matmul":61,"../graph_util":17,"./op":33}],31:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var conv_util = require("../../math/conv_util");
+var util = require("../../util");
+var op_1 = require("./op");
+var MaxPool = (function (_super) {
+    __extends(MaxPool, _super);
+    function MaxPool(xTensor, yTensor, fieldSize, stride, pad) {
+        if (stride === void 0) { stride = 1; }
+        var _this = _super.call(this) || this;
+        _this.xTensor = xTensor;
+        _this.yTensor = yTensor;
+        _this.fieldSize = fieldSize;
+        _this.stride = stride;
+        if (pad != null) {
+            _this.pad = pad;
+        }
+        else {
+            _this.pad = conv_util.computeDefaultPad(xTensor.shape, _this.fieldSize, _this.stride);
+        }
+        util.assert(util.isInt(_this.pad), "The zero padding (" + _this.pad + ") must be an integer. Change the " +
+            "stride and/or zero pad parameters");
+        return _this;
+    }
+    MaxPool.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(math.maxPool(x, _this.fieldSize, _this.stride, _this.pad)));
+        });
+    };
+    MaxPool.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        var dy = gradientArrays.get(this.yTensor);
+        math.scope(function () {
+            gradientArrays.add(_this.xTensor, math.maxPoolBackprop(dy, x, _this.fieldSize, _this.stride, _this.pad));
+        });
+    };
+    return MaxPool;
+}(op_1.Operation));
+exports.MaxPool = MaxPool;
+
+},{"../../math/conv_util":92,"../../util":101,"./op":33}],32:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../../util");
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var Multiply = (function (_super) {
+    __extends(Multiply, _super);
+    function Multiply(x1Tensor, x2Tensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.x1Tensor = x1Tensor;
+        _this.x2Tensor = x2Tensor;
+        _this.yTensor = yTensor;
+        util.assert(util.sizeFromShape(x1Tensor.shape) === 1 ||
+            util.sizeFromShape(x2Tensor.shape) === 1 ||
+            util.arraysEqual(x1Tensor.shape, x2Tensor.shape), 'One of t1 or t2 must be a scalar, or t1 and t2 must have ' +
+            'the same shape');
+        return _this;
+    }
+    Multiply.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var t1 = inferenceArrays.get(this.x1Tensor);
+        var t2 = inferenceArrays.get(this.x2Tensor);
+        math.scope(function (keep) {
+            var result;
+            if (util.isScalarShape(t1.shape)) {
+                result = math.scalarTimesArray(t1, t2);
+            }
+            else if (util.isScalarShape(t2.shape)) {
+                result = math.scalarTimesArray(t2, t1);
+            }
+            else {
+                result = math.elementWiseMul(t1, t2);
+            }
+            inferenceArrays.set(_this.yTensor, keep(result));
+        });
+    };
+    Multiply.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var x1 = inferenceArrays.get(this.x1Tensor);
+        var x2 = inferenceArrays.get(this.x2Tensor);
+        var dy = gradientArrays.get(this.yTensor);
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.x1Tensor)) {
+                if (util.isScalarShape(_this.x1Tensor.shape)) {
+                    var mul = math.elementWiseMul(dy, x2);
+                    gradientArrays.add(_this.x1Tensor, math.sum(mul));
+                }
+                else if (util.isScalarShape(x2.shape)) {
+                    gradientArrays.add(_this.x1Tensor, math.scalarTimesArray(x2, dy));
+                }
+                else {
+                    gradientArrays.add(_this.x1Tensor, math.elementWiseMul(x2, dy));
+                }
+            }
+            if (graph_util.shouldBackProp(_this.x2Tensor)) {
+                if (util.isScalarShape(_this.x2Tensor.shape)) {
+                    var mul = math.elementWiseMul(dy, x1);
+                    gradientArrays.add(_this.x2Tensor, math.sum(mul));
+                }
+                else if (util.isScalarShape(x1.shape)) {
+                    gradientArrays.add(_this.x2Tensor, math.scalarTimesArray(x1, dy));
+                }
+                else {
+                    gradientArrays.add(_this.x2Tensor, math.elementWiseMul(x1, dy));
+                }
+            }
+        });
+    };
+    return Multiply;
+}(op_1.Operation));
+exports.Multiply = Multiply;
+
+},{"../../util":101,"../graph_util":17,"./op":33}],33:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var Operation = (function () {
+    function Operation() {
+    }
+    Operation.prototype.disposeTransientArrays = function (inferenceArrays, gradientArrays) { };
+    Operation.prototype.dispose = function () { };
+    return Operation;
+}());
+exports.Operation = Operation;
+
+},{}],34:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../environment");
+var ndarray_1 = require("../../math/ndarray");
+var util = require("../../util");
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var ReduceSum = (function (_super) {
+    __extends(ReduceSum, _super);
+    function ReduceSum(x, outTensor) {
+        var _this = _super.call(this) || this;
+        _this.x = x;
+        _this.outTensor = outTensor;
+        util.assertShapesMatch(outTensor.shape, []);
+        _this.ones = environment_1.ENV.math.keep(ndarray_1.NDArray.ones(x.shape));
+        return _this;
+    }
+    ReduceSum.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.x);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.outTensor, keep(math.sum(x)));
+        });
+    };
+    ReduceSum.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        if (!graph_util.shouldBackProp(this.x)) {
+            return;
+        }
+        math.scope(function () {
+            var dy = gradientArrays.get(_this.outTensor);
+            gradientArrays.add(_this.x, math.scalarTimesArray(dy, _this.ones));
+        });
+    };
+    ReduceSum.prototype.dispose = function () {
+        this.ones.dispose();
+    };
+    return ReduceSum;
+}(op_1.Operation));
+exports.ReduceSum = ReduceSum;
+
+},{"../../environment":15,"../../math/ndarray":95,"../../util":101,"../graph_util":17,"./op":33}],35:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../../util");
+var op_1 = require("./op");
+var Reshape = (function (_super) {
+    __extends(Reshape, _super);
+    function Reshape(xTensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.xTensor = xTensor;
+        _this.yTensor = yTensor;
+        var xSize = util.sizeFromShape(xTensor.shape);
+        var ySize = util.sizeFromShape(yTensor.shape);
+        util.assert(xSize === ySize, "The input size (" + xSize + ") and output size (" + ySize + ") must match");
+        return _this;
+    }
+    Reshape.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var x = inferenceArrays.get(this.xTensor);
+        var clone = math.clone(x);
+        math.scope(function (keep) {
+            inferenceArrays.set(_this.yTensor, keep(clone.reshape(_this.yTensor.shape)));
+        });
+    };
+    Reshape.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var dy = gradientArrays.get(this.yTensor);
+        var clone = math.clone(dy);
+        math.scope(function () {
+            gradientArrays.add(_this.xTensor, clone.reshape(_this.xTensor.shape));
+        });
+    };
+    return Reshape;
+}(op_1.Operation));
+exports.Reshape = Reshape;
+
+},{"../../util":101,"./op":33}],36:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../environment");
+var ndarray_1 = require("../../math/ndarray");
+var util = require("../../util");
+var graph_1 = require("../graph");
+var op_1 = require("./op");
+var Softmax = (function (_super) {
+    __extends(Softmax, _super);
+    function Softmax(logitsTensor, output) {
+        var _this = _super.call(this) || this;
+        _this.logitsTensor = logitsTensor;
+        _this.output = output;
+        return _this;
+    }
+    Softmax.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var logits = inferenceArrays.get(this.logitsTensor);
+        return math.scope(function (keep) {
+            inferenceArrays.set(_this.output, keep(math.softmax(logits)));
+        });
+    };
+    Softmax.prototype.backProp = function () {
+        throw Error('Softmax backprop is not yet implemented');
+    };
+    return Softmax;
+}(op_1.Operation));
+exports.Softmax = Softmax;
+var SoftmaxCrossEntropyCost = (function (_super) {
+    __extends(SoftmaxCrossEntropyCost, _super);
+    function SoftmaxCrossEntropyCost(logitsTensor, labelTensor, yTensor) {
+        var _this = _super.call(this) || this;
+        _this.logitsTensor = logitsTensor;
+        _this.labelTensor = labelTensor;
+        _this.yTensor = yTensor;
+        _this.softmaxTensor = new graph_1.Tensor(logitsTensor.shape);
+        _this.epsilon = environment_1.ENV.math.keep(ndarray_1.Scalar.new(1e-5));
+        return _this;
+    }
+    SoftmaxCrossEntropyCost.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var logits = inferenceArrays.get(this.logitsTensor);
+        var label = inferenceArrays.get(this.labelTensor);
+        math.scope(function (keep) {
+            var softmaxResult = math.softmax(logits);
+            inferenceArrays.set(_this.softmaxTensor, keep(softmaxResult));
+            inferenceArrays.set(_this.yTensor, keep(crossEntropyCost(math, softmaxResult, label, _this.epsilon)));
+        });
+    };
+    SoftmaxCrossEntropyCost.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var softmax = inferenceArrays.get(this.softmaxTensor);
+        var label = inferenceArrays.get(this.labelTensor);
+        math.scope(function () {
+            gradientArrays.add(_this.logitsTensor, math.subtract(softmax, label));
+        });
+    };
+    SoftmaxCrossEntropyCost.prototype.disposeTransientArrays = function (inferenceArrays, gradientArrays) {
+        inferenceArrays.disposeArray(this.softmaxTensor);
+    };
+    SoftmaxCrossEntropyCost.prototype.dispose = function () {
+        this.epsilon.dispose();
+    };
+    return SoftmaxCrossEntropyCost;
+}(op_1.Operation));
+exports.SoftmaxCrossEntropyCost = SoftmaxCrossEntropyCost;
+function crossEntropyCost(math, y, target, epsilon) {
+    util.assert(y.size === target.size, 'The output and target must be the same size');
+    return math.scope(function () {
+        var yPlusEps = math.scalarPlusArray(epsilon, y);
+        var logOutput = math.log(yPlusEps);
+        var tarLogOutput = math.elementWiseMul(target, logOutput);
+        var costVector = math.neg(tarLogOutput);
+        return math.sum(costVector);
+    });
+}
+exports.crossEntropyCost = crossEntropyCost;
+
+},{"../../environment":15,"../../math/ndarray":95,"../../util":101,"../graph":16,"./op":33}],37:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../../util");
+var graph_util = require("../graph_util");
+var op_1 = require("./op");
+var Subtract = (function (_super) {
+    __extends(Subtract, _super);
+    function Subtract(t1, t2, outTensor) {
+        var _this = _super.call(this) || this;
+        _this.t1 = t1;
+        _this.t2 = t2;
+        _this.outTensor = outTensor;
+        util.assert(util.sizeFromShape(t1.shape) === 1 ||
+            util.sizeFromShape(t2.shape) === 1 ||
+            util.arraysEqual(t1.shape, t2.shape), 'One of t1 or t2 must be a scalar, or t1 and t2 must have ' +
+            'the same shape');
+        return _this;
+    }
+    Subtract.prototype.feedForward = function (math, inferenceArrays) {
+        var _this = this;
+        var t1 = inferenceArrays.get(this.t1);
+        var t2 = inferenceArrays.get(this.t2);
+        math.scope(function (keep) {
+            var result;
+            if (util.isScalarShape(t1.shape)) {
+                result = math.scalarMinusArray(t1, t2);
+            }
+            else if (util.isScalarShape(t2.shape)) {
+                result = math.arrayMinusScalar(t1, t2);
+            }
+            else {
+                result = math.subtract(t1, t2);
+            }
+            inferenceArrays.set(_this.outTensor, keep(result));
+        });
+    };
+    Subtract.prototype.backProp = function (math, inferenceArrays, gradientArrays) {
+        var _this = this;
+        var dy = gradientArrays.get(this.outTensor);
+        math.scope(function () {
+            if (graph_util.shouldBackProp(_this.t1)) {
+                if (util.isScalarShape(_this.t1.shape)) {
+                    var sum = math.sum(dy);
+                    gradientArrays.add(_this.t1, sum);
+                }
+                else {
+                    gradientArrays.add(_this.t1, math.clone(dy));
+                }
+            }
+            if (graph_util.shouldBackProp(_this.t2)) {
+                if (util.isScalarShape(_this.t2.shape)) {
+                    var sum = math.sum(dy);
+                    var negSum = math.neg(sum);
+                    gradientArrays.add(_this.t2, negSum);
+                }
+                else {
+                    gradientArrays.add(_this.t2, math.neg(dy));
+                }
+            }
+        });
+    };
+    Subtract.prototype.dispose = function () {
+        if (this.dySizeScalar != null) {
+            this.dySizeScalar.dispose();
+        }
+    };
+    return Subtract;
+}(op_1.Operation));
+exports.Subtract = Subtract;
+
+},{"../../util":101,"../graph_util":17,"./op":33}],38:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../../math/ndarray");
+var tensor_array_map_1 = require("../tensor_array_map");
+var optimizer_1 = require("./optimizer");
+var AdadeltaOptimizer = (function (_super) {
+    __extends(AdadeltaOptimizer, _super);
+    function AdadeltaOptimizer(learningRate, gamma, specifiedVariableList) {
+        var _this = _super.call(this, learningRate, specifiedVariableList) || this;
+        _this.learningRate = learningRate;
+        _this.gamma = gamma;
+        _this.accumulatedSquaredGradients = new tensor_array_map_1.TensorArrayMap();
+        _this.accumulatedUpdates = new tensor_array_map_1.TensorArrayMap();
+        _this.eps = ndarray_1.Scalar.new(1e-6);
+        _this.g = ndarray_1.Scalar.new(_this.gamma);
+        return _this;
+    }
+    AdadeltaOptimizer.prototype.beforeBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        _super.prototype.beforeBatch.call(this, math, batchSize, runtime, activationArrayMap, gradientArrayMap);
+        if (this.accumulatedSquaredGradients.size() === 0) {
+            this.variableNodes.forEach(function (node) {
+                _this.accumulatedSquaredGradients.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+                _this.accumulatedUpdates.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+            });
+        }
+    };
+    AdadeltaOptimizer.prototype.afterBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        math.scope(function (keep) {
+            _this.variableNodes.forEach(function (node) {
+                var oldVariable = activationArrayMap.get(node.output);
+                var gradient = _this.variableGradients.get(node.output);
+                var oldCache = _this.accumulatedSquaredGradients.get(node.output);
+                var oldUpdates = _this.accumulatedUpdates.get(node.output);
+                var gradientSquare = math.multiply(gradient, gradient);
+                var cache = math.scaledArrayAdd(_this.g, oldCache, math.subtract(_this.one, _this.g), gradientSquare);
+                var updates = math.multiply(math.divide(math.sqrt(math.add(oldUpdates, _this.eps)), math.sqrt(math.add(oldCache, _this.eps))), gradient);
+                var variable = math.scaledArrayAdd(_this.c, updates, _this.one, oldVariable);
+                var updateSquare = math.multiply(updates, updates);
+                var newUpdates = math.scaledArrayAdd(_this.g, oldUpdates, math.subtract(_this.one, _this.g), updateSquare);
+                _this.accumulatedSquaredGradients.set(node.output, keep(cache));
+                _this.accumulatedUpdates.set(node.output, keep(newUpdates));
+                activationArrayMap.set(node.output, keep(variable));
+                node.data = variable;
+                oldVariable.dispose();
+                oldCache.dispose();
+                oldUpdates.dispose();
+            });
+        });
+        this.variableGradients.dispose();
+        this.variableGradients = new tensor_array_map_1.TensorArrayMap();
+    };
+    AdadeltaOptimizer.prototype.dispose = function () {
+        _super.prototype.dispose.call(this);
+        this.eps.dispose();
+        this.g.dispose();
+        this.accumulatedSquaredGradients.dispose();
+        this.accumulatedUpdates.dispose();
+    };
+    return AdadeltaOptimizer;
+}(optimizer_1.Optimizer));
+exports.AdadeltaOptimizer = AdadeltaOptimizer;
+
+},{"../../math/ndarray":95,"../tensor_array_map":49,"./optimizer":43}],39:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../../math/ndarray");
+var tensor_array_map_1 = require("../tensor_array_map");
+var optimizer_1 = require("./optimizer");
+var AdagradOptimizer = (function (_super) {
+    __extends(AdagradOptimizer, _super);
+    function AdagradOptimizer(learningRate, specifiedVariableList) {
+        var _this = _super.call(this, learningRate, specifiedVariableList) || this;
+        _this.learningRate = learningRate;
+        _this.accumulatedSquaredGradients = new tensor_array_map_1.TensorArrayMap();
+        _this.eps = ndarray_1.Scalar.new(1e-6);
+        return _this;
+    }
+    AdagradOptimizer.prototype.beforeBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        _super.prototype.beforeBatch.call(this, math, batchSize, runtime, activationArrayMap, gradientArrayMap);
+        if (this.accumulatedSquaredGradients.size() === 0) {
+            this.variableNodes.forEach(function (node) {
+                _this.accumulatedSquaredGradients.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+            });
+        }
+    };
+    AdagradOptimizer.prototype.afterBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        math.scope(function (keep) {
+            _this.variableNodes.forEach(function (node) {
+                var oldVariable = activationArrayMap.get(node.output);
+                var gradient = _this.variableGradients.get(node.output);
+                var oldCache = _this.accumulatedSquaredGradients.get(node.output);
+                var gradientSquare = math.multiply(gradient, gradient);
+                var cache = math.add(oldCache, gradientSquare);
+                var variable = math.scaledArrayAdd(_this.c, math.divide(gradient, math.add(math.sqrt(cache), _this.eps)), _this.one, oldVariable);
+                _this.accumulatedSquaredGradients.set(node.output, keep(cache));
+                activationArrayMap.set(node.output, keep(variable));
+                node.data = variable;
+                oldVariable.dispose();
+                oldCache.dispose();
+            });
+        });
+        this.variableGradients.dispose();
+        this.variableGradients = new tensor_array_map_1.TensorArrayMap();
+    };
+    AdagradOptimizer.prototype.dispose = function () {
+        _super.prototype.dispose.call(this);
+        this.eps.dispose();
+        this.accumulatedSquaredGradients.dispose();
+    };
+    return AdagradOptimizer;
+}(optimizer_1.Optimizer));
+exports.AdagradOptimizer = AdagradOptimizer;
+
+},{"../../math/ndarray":95,"../tensor_array_map":49,"./optimizer":43}],40:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../../math/ndarray");
+var tensor_array_map_1 = require("../tensor_array_map");
+var optimizer_1 = require("./optimizer");
+var AdamOptimizer = (function (_super) {
+    __extends(AdamOptimizer, _super);
+    function AdamOptimizer(learningRate, beta1, beta2, specifiedVariableList) {
+        var _this = _super.call(this, learningRate, specifiedVariableList) || this;
+        _this.learningRate = learningRate;
+        _this.beta1 = beta1;
+        _this.beta2 = beta2;
+        _this.firstMoment = new tensor_array_map_1.TensorArrayMap();
+        _this.secondMoment = new tensor_array_map_1.TensorArrayMap();
+        _this.eps = ndarray_1.Scalar.new(1e-8);
+        _this.b1 = ndarray_1.Scalar.new(_this.beta1);
+        _this.b2 = ndarray_1.Scalar.new(_this.beta2);
+        _this.accB1 = ndarray_1.Scalar.new(_this.beta1);
+        _this.accB2 = ndarray_1.Scalar.new(_this.beta2);
+        return _this;
+    }
+    AdamOptimizer.prototype.beforeBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        _super.prototype.beforeBatch.call(this, math, batchSize, runtime, activationArrayMap, gradientArrayMap);
+        if (this.firstMoment.size() === 0) {
+            this.variableNodes.forEach(function (node) {
+                _this.firstMoment.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+            });
+        }
+        if (this.secondMoment.size() === 0) {
+            this.variableNodes.forEach(function (node) {
+                _this.secondMoment.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+            });
+        }
+    };
+    AdamOptimizer.prototype.afterBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        math.scope(function (keep) {
+            _this.variableNodes.forEach(function (node) {
+                var oldVariable = activationArrayMap.get(node.output);
+                var gradient = _this.variableGradients.get(node.output);
+                var oldFirstMoment = _this.firstMoment.get(node.output);
+                var oldSecondMoment = _this.secondMoment.get(node.output);
+                var newFirstMoment = math.scaledArrayAdd(_this.b1, oldFirstMoment, math.subtract(_this.one, _this.b1), gradient);
+                var gradientSquare = math.multiply(gradient, gradient);
+                var newSecondMoment = math.scaledArrayAdd(_this.b2, oldSecondMoment, math.subtract(_this.one, _this.b2), gradientSquare);
+                var biasCorrectedFirstMoment = math.divide(newFirstMoment, math.subtract(_this.one, _this.accB1));
+                var biasCorrectedSecondMoment = math.divide(newSecondMoment, math.subtract(_this.one, _this.accB2));
+                var variable = math.scaledArrayAdd(_this.c, math.divide(biasCorrectedFirstMoment, math.add(math.sqrt(biasCorrectedSecondMoment), _this.eps)), _this.one, oldVariable);
+                activationArrayMap.set(node.output, keep(variable));
+                node.data = variable;
+                _this.firstMoment.set(node.output, keep(newFirstMoment));
+                _this.secondMoment.set(node.output, keep(newSecondMoment));
+                oldVariable.dispose();
+                gradient.dispose();
+                oldFirstMoment.dispose();
+                oldSecondMoment.dispose();
+            });
+            var oldAccB1 = _this.accB1;
+            var oldAccB2 = _this.accB2;
+            _this.accB1 = keep(math.multiply(_this.accB1, _this.b1));
+            _this.accB2 = keep(math.multiply(_this.accB2, _this.b2));
+            oldAccB1.dispose();
+            oldAccB2.dispose();
+        });
+        this.variableGradients.dispose();
+        this.variableGradients = new tensor_array_map_1.TensorArrayMap();
+    };
+    AdamOptimizer.prototype.dispose = function () {
+        _super.prototype.dispose.call(this);
+        this.firstMoment.dispose();
+        this.secondMoment.dispose();
+        this.eps.dispose();
+        this.b1.dispose();
+        this.b2.dispose();
+        this.accB1.dispose();
+        this.accB2.dispose();
+    };
+    return AdamOptimizer;
+}(optimizer_1.Optimizer));
+exports.AdamOptimizer = AdamOptimizer;
+
+},{"../../math/ndarray":95,"../tensor_array_map":49,"./optimizer":43}],41:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../../math/ndarray");
+var tensor_array_map_1 = require("../tensor_array_map");
+var optimizer_1 = require("./optimizer");
+var AdamaxOptimizer = (function (_super) {
+    __extends(AdamaxOptimizer, _super);
+    function AdamaxOptimizer(learningRate, beta1, beta2, specifiedVariableList) {
+        var _this = _super.call(this, learningRate, specifiedVariableList) || this;
+        _this.learningRate = learningRate;
+        _this.beta1 = beta1;
+        _this.beta2 = beta2;
+        _this.firstMoment = new tensor_array_map_1.TensorArrayMap();
+        _this.weightedInfNorm = new tensor_array_map_1.TensorArrayMap();
+        _this.eps = ndarray_1.Scalar.new(1e-8);
+        _this.b1 = ndarray_1.Scalar.new(_this.beta1);
+        _this.b2 = ndarray_1.Scalar.new(_this.beta2);
+        _this.accB1 = ndarray_1.Scalar.new(_this.beta1);
+        return _this;
+    }
+    AdamaxOptimizer.prototype.beforeBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        _super.prototype.beforeBatch.call(this, math, batchSize, runtime, activationArrayMap, gradientArrayMap);
+        if (this.firstMoment.size() === 0) {
+            this.variableNodes.forEach(function (node) {
+                _this.firstMoment.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+            });
+        }
+        if (this.weightedInfNorm.size() === 0) {
+            this.variableNodes.forEach(function (node) {
+                _this.weightedInfNorm.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+            });
+        }
+    };
+    AdamaxOptimizer.prototype.afterBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        math.scope(function (keep) {
+            _this.variableNodes.forEach(function (node) {
+                var oldVariable = activationArrayMap.get(node.output);
+                var gradient = _this.variableGradients.get(node.output);
+                var oldFirstMoment = _this.firstMoment.get(node.output);
+                var oldWeightedInfNorm = _this.weightedInfNorm.get(node.output);
+                var newFirstMoment = math.scaledArrayAdd(_this.b1, oldFirstMoment, math.subtract(_this.one, _this.b1), gradient);
+                var ut0 = math.multiply(_this.b2, oldWeightedInfNorm);
+                var ut1 = math.abs(gradient);
+                var newWeightedInfNorm = math.add(math.relu(math.subtract(ut0, ut1)), ut1);
+                var variable = math.scaledArrayAdd(_this.one, oldVariable, math.divide(_this.c, math.subtract(_this.one, _this.accB1)), math.divide(newFirstMoment, math.add(_this.eps, newWeightedInfNorm)));
+                activationArrayMap.set(node.output, keep(variable));
+                node.data = variable;
+                _this.firstMoment.set(node.output, keep(newFirstMoment));
+                _this.weightedInfNorm.set(node.output, keep(newWeightedInfNorm));
+                oldVariable.dispose();
+                gradient.dispose();
+                oldFirstMoment.dispose();
+                oldWeightedInfNorm.dispose();
+            });
+            var oldAccB1 = _this.accB1;
+            _this.accB1 = keep(math.multiply(_this.accB1, _this.b1));
+            oldAccB1.dispose();
+        });
+        this.variableGradients.dispose();
+        this.variableGradients = new tensor_array_map_1.TensorArrayMap();
+    };
+    AdamaxOptimizer.prototype.dispose = function () {
+        _super.prototype.dispose.call(this);
+        this.firstMoment.dispose();
+        this.weightedInfNorm.dispose();
+        this.eps.dispose();
+        this.accB1.dispose();
+        this.b1.dispose();
+        this.b2.dispose();
+    };
+    return AdamaxOptimizer;
+}(optimizer_1.Optimizer));
+exports.AdamaxOptimizer = AdamaxOptimizer;
+
+},{"../../math/ndarray":95,"../tensor_array_map":49,"./optimizer":43}],42:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../../math/ndarray");
+var tensor_array_map_1 = require("../tensor_array_map");
+var sgd_optimizer_1 = require("./sgd_optimizer");
+var MomentumOptimizer = (function (_super) {
+    __extends(MomentumOptimizer, _super);
+    function MomentumOptimizer(learningRate, momentum, specifiedVariableList) {
+        var _this = _super.call(this, learningRate, specifiedVariableList) || this;
+        _this.learningRate = learningRate;
+        _this.momentum = momentum;
+        _this.variableVelocities = new tensor_array_map_1.TensorArrayMap();
+        _this.m = ndarray_1.Scalar.new(_this.momentum);
+        return _this;
+    }
+    MomentumOptimizer.prototype.beforeBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        _super.prototype.beforeBatch.call(this, math, batchSize, runtime, activationArrayMap, gradientArrayMap);
+        if (this.variableVelocities.size() === 0) {
+            this.variableNodes.forEach(function (node) {
+                _this.variableVelocities.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+            });
+        }
+    };
+    MomentumOptimizer.prototype.afterBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        math.scope(function (keep) {
+            _this.variableNodes.forEach(function (node) {
+                var oldVariable = activationArrayMap.get(node.output);
+                var gradient = _this.variableGradients.get(node.output);
+                var oldVelocity = _this.variableVelocities.get(node.output);
+                var velocity = math.scaledArrayAdd(_this.m, oldVelocity, _this.one, gradient);
+                var variable = math.scaledArrayAdd(_this.c, velocity, _this.one, oldVariable);
+                _this.variableVelocities.set(node.output, keep(velocity));
+                activationArrayMap.set(node.output, keep(variable));
+                node.data = variable;
+                oldVariable.dispose();
+                oldVelocity.dispose();
+            });
+        });
+        this.variableGradients.dispose();
+        this.variableGradients = new tensor_array_map_1.TensorArrayMap();
+    };
+    MomentumOptimizer.prototype.dispose = function () {
+        _super.prototype.dispose.call(this);
+        this.m.dispose();
+        this.variableVelocities.dispose();
+    };
+    MomentumOptimizer.prototype.setMomentum = function (momentum) {
+        this.momentum = momentum;
+    };
+    return MomentumOptimizer;
+}(sgd_optimizer_1.SGDOptimizer));
+exports.MomentumOptimizer = MomentumOptimizer;
+
+},{"../../math/ndarray":95,"../tensor_array_map":49,"./sgd_optimizer":45}],43:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../environment");
+var ndarray_1 = require("../../math/ndarray");
+var session_util = require("../session_util");
+var tensor_array_map_1 = require("../tensor_array_map");
+var Optimizer = (function () {
+    function Optimizer(learningRate, specifiedVariableList) {
+        this.learningRate = learningRate;
+        this.variableGradients = new tensor_array_map_1.TensorArrayMap();
+        if (specifiedVariableList != null) {
+            this.specifiedVariableNodes = specifiedVariableList;
+        }
+        this.one = environment_1.ENV.math.keep(ndarray_1.Scalar.new(1));
+    }
+    Optimizer.prototype.beforeBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        this.variableNodes = this.specifiedVariableNodes == null ?
+            session_util.getVariableNodesFromEvaluationSet(runtime.nodes) :
+            this.specifiedVariableNodes;
+        if (batchSize !== this.prevBatchSize) {
+            if (this.c != null) {
+                this.c.dispose();
+            }
+            this.prevBatchSize = batchSize;
+            this.c = math.keep(ndarray_1.Scalar.new(-this.learningRate / batchSize));
+        }
+        this.variableNodes.forEach(function (node) { return _this.variableGradients.set(node.output, math.keep(ndarray_1.NDArray.zeros(node.output.shape))); });
+    };
+    Optimizer.prototype.afterExample = function (math, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        math.scope(function (keep) {
+            _this.variableNodes.forEach(function (node) {
+                var gradient = gradientArrayMap.get(node.output);
+                var accumulatedGradient = _this.variableGradients.get(node.output);
+                _this.variableGradients.set(node.output, keep(math.add(gradient, accumulatedGradient)));
+                accumulatedGradient.dispose();
+            });
+        });
+    };
+    Optimizer.prototype.dispose = function () {
+        if (this.c != null) {
+            this.c.dispose();
+        }
+        this.one.dispose();
+        this.variableNodes.forEach(function (node) {
+            node.data.dispose();
+        });
+        this.specifiedVariableNodes.forEach(function (node) {
+            node.data.dispose();
+        });
+    };
+    return Optimizer;
+}());
+exports.Optimizer = Optimizer;
+
+},{"../../environment":15,"../../math/ndarray":95,"../session_util":48,"../tensor_array_map":49}],44:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../../math/ndarray");
+var tensor_array_map_1 = require("../tensor_array_map");
+var optimizer_1 = require("./optimizer");
+var RMSPropOptimizer = (function (_super) {
+    __extends(RMSPropOptimizer, _super);
+    function RMSPropOptimizer(learningRate, gamma, specifiedVariableList) {
+        var _this = _super.call(this, learningRate, specifiedVariableList) || this;
+        _this.learningRate = learningRate;
+        _this.gamma = gamma;
+        _this.accumulatedSquaredGradients = new tensor_array_map_1.TensorArrayMap();
+        _this.eps = ndarray_1.Scalar.new(1e-6);
+        _this.g = ndarray_1.Scalar.new(_this.gamma);
+        return _this;
+    }
+    RMSPropOptimizer.prototype.beforeBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        _super.prototype.beforeBatch.call(this, math, batchSize, runtime, activationArrayMap, gradientArrayMap);
+        if (this.accumulatedSquaredGradients.size() === 0) {
+            this.variableNodes.forEach(function (node) {
+                _this.accumulatedSquaredGradients.set(node.output, ndarray_1.NDArray.zeros(node.output.shape));
+            });
+        }
+    };
+    RMSPropOptimizer.prototype.afterBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        math.scope(function (keep) {
+            _this.variableNodes.forEach(function (node) {
+                var oldVariable = activationArrayMap.get(node.output);
+                var gradient = _this.variableGradients.get(node.output);
+                var oldCache = _this.accumulatedSquaredGradients.get(node.output);
+                var gradientSquare = math.multiply(gradient, gradient);
+                var cache = math.scaledArrayAdd(_this.g, oldCache, math.subtract(_this.one, _this.g), gradientSquare);
+                var variable = math.scaledArrayAdd(_this.c, math.divide(gradient, math.add(math.sqrt(cache), _this.eps)), _this.one, oldVariable);
+                _this.accumulatedSquaredGradients.set(node.output, keep(cache));
+                activationArrayMap.set(node.output, keep(variable));
+                node.data = variable;
+                oldVariable.dispose();
+                oldCache.dispose();
+            });
+        });
+        this.variableGradients.dispose();
+        this.variableGradients = new tensor_array_map_1.TensorArrayMap();
+    };
+    RMSPropOptimizer.prototype.dispose = function () {
+        _super.prototype.dispose.call(this);
+        this.eps.dispose();
+        this.g.dispose();
+        this.accumulatedSquaredGradients.dispose();
+    };
+    return RMSPropOptimizer;
+}(optimizer_1.Optimizer));
+exports.RMSPropOptimizer = RMSPropOptimizer;
+
+},{"../../math/ndarray":95,"../tensor_array_map":49,"./optimizer":43}],45:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var tensor_array_map_1 = require("../tensor_array_map");
+var optimizer_1 = require("./optimizer");
+var SGDOptimizer = (function (_super) {
+    __extends(SGDOptimizer, _super);
+    function SGDOptimizer(learningRate, specifiedVariableList) {
+        var _this = _super.call(this, learningRate, specifiedVariableList) || this;
+        _this.learningRate = learningRate;
+        return _this;
+    }
+    SGDOptimizer.prototype.afterBatch = function (math, batchSize, runtime, activationArrayMap, gradientArrayMap) {
+        var _this = this;
+        math.scope(function (keep) {
+            _this.variableNodes.forEach(function (node) {
+                var oldVariable = activationArrayMap.get(node.output);
+                var gradient = _this.variableGradients.get(node.output);
+                var variable = math.scaledArrayAdd(_this.c, gradient, _this.one, oldVariable);
+                activationArrayMap.set(node.output, keep(variable));
+                node.data = variable;
+                oldVariable.dispose();
+            });
+        });
+        this.variableGradients.dispose();
+        this.variableGradients = new tensor_array_map_1.TensorArrayMap();
+    };
+    SGDOptimizer.prototype.dispose = function () {
+        _super.prototype.dispose.call(this);
+    };
+    SGDOptimizer.prototype.setLearningRate = function (learningRate) {
+        this.learningRate = learningRate;
+    };
+    return SGDOptimizer;
+}(optimizer_1.Optimizer));
+exports.SGDOptimizer = SGDOptimizer;
+
+},{"../tensor_array_map":49,"./optimizer":43}],46:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+function defaultCompare(a, b) {
+    if (a === b) {
+        return 0;
+    }
+    else if (a < b) {
+        return -1;
+    }
+    else {
+        return 1;
+    }
+}
+exports.defaultCompare = defaultCompare;
+var PriorityQueue = (function () {
+    function PriorityQueue(comparator, indexObserver) {
+        this.comparator = comparator;
+        this.indexObserver = indexObserver;
+        this.heap = [];
+    }
+    PriorityQueue.prototype.enqueue = function (t) {
+        this.heap.push(t);
+        this.onIndexChanged(t, this.heap.length - 1);
+        this.siftUp(this.heap.length - 1);
+    };
+    PriorityQueue.prototype.dequeue = function () {
+        if (this.empty()) {
+            throw new Error('dequeue called on empty priority queue.');
+        }
+        var t = this.heap[0];
+        this.swap(0, this.heap.length - 1);
+        this.heap.pop();
+        this.siftDown(0);
+        return t;
+    };
+    PriorityQueue.prototype.update = function (newT, index) {
+        var last = (index === this.heap.length - 1);
+        if (!last) {
+            this.swap(index, this.heap.length - 1);
+        }
+        this.heap.pop();
+        if (!last) {
+            if (this.siftUpIndex(index) !== -1) {
+                this.siftUp(index);
+            }
+            else if (this.siftDownIndex(index) !== -1) {
+                this.siftDown(index);
+            }
+        }
+        this.enqueue(newT);
+    };
+    PriorityQueue.prototype.empty = function () {
+        return this.heap.length === 0;
+    };
+    PriorityQueue.prototype.onIndexChanged = function (t, newIndex) {
+        if (this.indexObserver) {
+            this.indexObserver(t, newIndex);
+        }
+    };
+    PriorityQueue.prototype.getParentIndex = function (index) {
+        if (index === 0) {
+            return -1;
+        }
+        return Math.floor((index - 1) / 2);
+    };
+    PriorityQueue.prototype.getLeftChildIndex = function (index) {
+        var candidate = index * 2 + 1;
+        return candidate < this.heap.length ? candidate : -1;
+    };
+    PriorityQueue.prototype.getRightChildIndex = function (index) {
+        var candidate = index * 2 + 2;
+        return candidate < this.heap.length ? candidate : -1;
+    };
+    PriorityQueue.prototype.siftUpIndex = function (index) {
+        var parentIndex = this.getParentIndex(index);
+        if (parentIndex === -1) {
+            return -1;
+        }
+        if (this.compare(parentIndex, index) > 0) {
+            return parentIndex;
+        }
+        return -1;
+    };
+    PriorityQueue.prototype.siftUp = function (index) {
+        var siftIndex = this.siftUpIndex(index);
+        while (siftIndex !== -1) {
+            this.swap(index, siftIndex);
+            index = siftIndex;
+            siftIndex = this.siftUpIndex(index);
+        }
+    };
+    PriorityQueue.prototype.siftDownIndex = function (index) {
+        if (index >= this.heap.length) {
+            return -1;
+        }
+        var largestChildIndex = index;
+        var leftChildIndex = this.getLeftChildIndex(index);
+        if ((leftChildIndex !== -1) &&
+            (this.compare(leftChildIndex, largestChildIndex) < 0)) {
+            largestChildIndex = leftChildIndex;
+        }
+        var rightChildIndex = this.getRightChildIndex(index);
+        if ((rightChildIndex !== -1) &&
+            (this.compare(rightChildIndex, largestChildIndex) < 0)) {
+            largestChildIndex = rightChildIndex;
+        }
+        return (largestChildIndex === index) ? -1 : largestChildIndex;
+    };
+    PriorityQueue.prototype.siftDown = function (index) {
+        var siftIndex = this.siftDownIndex(index);
+        while (siftIndex !== -1) {
+            this.swap(index, siftIndex);
+            index = siftIndex;
+            siftIndex = this.siftDownIndex(index);
+        }
+    };
+    PriorityQueue.prototype.compare = function (aIndex, bIndex) {
+        return this.comparator(this.heap[aIndex], this.heap[bIndex]);
+    };
+    PriorityQueue.prototype.swap = function (a, b) {
+        var temp = this.heap[a];
+        this.heap[a] = this.heap[b];
+        this.heap[b] = temp;
+        this.onIndexChanged(this.heap[a], a);
+        this.onIndexChanged(this.heap[b], b);
+    };
+    return PriorityQueue;
+}());
+exports.PriorityQueue = PriorityQueue;
+
+},{}],47:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../math/ndarray");
+var util = require("../util");
+var operation_emitter = require("./operation_emitter");
+var session_util = require("./session_util");
+var tensor_array_map_1 = require("./tensor_array_map");
+var FeedDictionary = (function () {
+    function FeedDictionary(feedEntries) {
+        var _this = this;
+        this.dict = {};
+        if (feedEntries) {
+            feedEntries.forEach(function (entry) { return _this.dict[entry.tensor.id] = entry; });
+        }
+    }
+    return FeedDictionary;
+}());
+exports.FeedDictionary = FeedDictionary;
+var CostReduction;
+(function (CostReduction) {
+    CostReduction[CostReduction["NONE"] = 0] = "NONE";
+    CostReduction[CostReduction["SUM"] = 1] = "SUM";
+    CostReduction[CostReduction["MEAN"] = 2] = "MEAN";
+})(CostReduction = exports.CostReduction || (exports.CostReduction = {}));
+var Session = (function () {
+    function Session(graph, math) {
+        this.math = math;
+        this.activationArrayMap = new tensor_array_map_1.TensorArrayMap();
+        this.runtimeCache = {};
+        this.oneScalar = ndarray_1.Scalar.new(1);
+        this.gradientArrayMap = new tensor_array_map_1.SummedTensorArrayMap(this.math);
+    }
+    Session.prototype.dispose = function () {
+        var _this = this;
+        this.activationArrayMap.dispose();
+        Object.keys(this.runtimeCache).forEach(function (key) {
+            var runtime = _this.runtimeCache[key];
+            if (runtime.operations) {
+                runtime.operations.forEach(function (op) { return op.dispose(); });
+            }
+        });
+        this.runtimeCache = {};
+        if (this.batchSizeScalar != null) {
+            this.batchSizeScalar.dispose();
+        }
+        this.oneScalar.dispose();
+    };
+    Session.prototype.evalAll = function (tensors, feedEntries) {
+        var _this = this;
+        return this.math.scope(function () {
+            var feed = new FeedDictionary(feedEntries);
+            var runtime = _this.getOrCreateRuntime(tensors, feed);
+            var activations = _this.activationArrayMap;
+            session_util.disposeAndInitializeOperationOutputs(runtime.nodes, activations);
+            session_util.disposeTransientOperationArrays(runtime.operations, _this.activationArrayMap, _this.gradientArrayMap);
+            session_util.addPersistentArraysToTensorArrayMap(runtime.nodes, activations);
+            session_util.loadInputsFromFeedDictionaryToTensorArrayMap(feed, activations, _this.math);
+            runtime.operations.forEach(function (op) { return op.feedForward(_this.math, activations); });
+            var results = tensors.map(function (x) { return activations.get(x); });
+            tensors.forEach(function (x) { return activations.delete(x); });
+            session_util.releaseFeedDictionaryInputsFromTensorArrayMap(feed, activations, _this.math);
+            return results;
+        });
+    };
+    Session.prototype.eval = function (tensor, feedEntries) {
+        return this.evalAll([tensor], feedEntries)[0];
+    };
+    Session.prototype.train = function (costTensor, feedEntries, batchSize, optimizer, costReduction) {
+        var _this = this;
+        if (costReduction === void 0) { costReduction = CostReduction.NONE; }
+        util.assert(util.isScalarShape(costTensor.shape), 'Cost tensor for training must be a scalar value.');
+        if (this.prevBatchSize !== batchSize) {
+            this.prevBatchSize = batchSize;
+            if (this.batchSizeScalar != null) {
+                this.batchSizeScalar.dispose();
+            }
+            this.batchSizeScalar = this.math.keep(ndarray_1.Scalar.new(batchSize));
+        }
+        var feed = new FeedDictionary(feedEntries);
+        session_util.throwIfFeedDictionaryContainsNDArrays(feed);
+        var runtime = this.getOrCreateRuntime([costTensor], feed);
+        var inferenceOperations = runtime.operations;
+        var backPropOperations = runtime.operations.slice().reverse();
+        var activations = this.activationArrayMap;
+        var gradients = this.gradientArrayMap;
+        gradients.nullify(costTensor);
+        gradients.add(costTensor, this.oneScalar);
+        session_util.addPersistentArraysToTensorArrayMap(runtime.nodes, activations);
+        optimizer.beforeBatch(this.math, batchSize, runtime, activations, gradients);
+        return this.math.scope(function () {
+            var cost = ndarray_1.Scalar.new(0);
+            for (var i = 0; i < batchSize; ++i) {
+                session_util.disposeAndInitializeOperationOutputs(runtime.nodes, activations);
+                session_util.disposeAndInitializeOperationInputGradients(runtime.nodes, gradients);
+                session_util.disposeTransientOperationArrays(runtime.operations, activations, gradients);
+                session_util.loadInputsFromFeedDictionaryToTensorArrayMap(feed, activations, _this.math);
+                inferenceOperations.forEach(function (op) { return op.feedForward(_this.math, activations); });
+                backPropOperations.forEach(function (op) { return op.backProp(_this.math, activations, gradients); });
+                optimizer.afterExample(_this.math, runtime, activations, gradients);
+                session_util.releaseFeedDictionaryInputsFromTensorArrayMap(feed, activations, _this.math);
+                cost = _this.updateCostForExample(cost, activations.get(costTensor), costReduction);
+            }
+            optimizer.afterBatch(_this.math, batchSize, runtime, activations, gradients);
+            return _this.updateCostForBatch(cost, costReduction);
+        });
+    };
+    Session.prototype.updateCostForExample = function (totalCost, currCost, costReduction) {
+        if (costReduction === CostReduction.MEAN ||
+            costReduction === CostReduction.SUM) {
+            return this.math.add(totalCost, currCost);
+        }
+        return totalCost;
+    };
+    Session.prototype.updateCostForBatch = function (totalCost, costReduction) {
+        if (costReduction === CostReduction.MEAN) {
+            return this.math.divide(totalCost, this.batchSizeScalar);
+        }
+        return totalCost;
+    };
+    Session.prototype.getOrCreateRuntime = function (tensors, feed) {
+        var key = this.makeRuntimeCacheKey(tensors, feed);
+        var runtime = this.runtimeCache[key];
+        if (runtime === undefined) {
+            var nodes = session_util.getOrderedEvaluationSetFromEvalTensor(tensors, feed);
+            session_util.removeFeedDictionaryNodesFromEvaluationSet(feed, nodes);
+            session_util.throwErrorIfEvaluationSetContainsPlaceholderNodes(nodes);
+            var operations = operation_emitter.emitFromGraphNodes(nodes);
+            runtime = { nodes: nodes, operations: operations };
+            this.runtimeCache[key] = runtime;
+        }
+        return runtime;
+    };
+    Session.prototype.makeRuntimeCacheKey = function (tensors, feed) {
+        return tensors.map(function (x) { return x.id; }).sort().join('_') + '__' +
+            Object.keys(feed.dict).sort().join('_');
+    };
+    return Session;
+}());
+exports.Session = Session;
+
+},{"../math/ndarray":95,"../util":101,"./operation_emitter":18,"./session_util":48,"./tensor_array_map":49}],48:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../math/ndarray");
+var util = require("../util");
+var graph_1 = require("./graph");
+var graph_util = require("./graph_util");
+function getTerminatingNodesFromFeedDictionary(feedDictionary) {
+    return Object.keys(feedDictionary.dict)
+        .map(function (tensorID) { return feedDictionary.dict[+tensorID].tensor.node; });
+}
+exports.getTerminatingNodesFromFeedDictionary = getTerminatingNodesFromFeedDictionary;
+function getOrderedEvaluationSetFromEvalTensor(evalTensors, feedDictionary) {
+    var terminatingNodes = getTerminatingNodesFromFeedDictionary(feedDictionary);
+    var evalNodes = evalTensors.map(function (x) { return x.node; });
+    var unorderedEvaluationSet = graph_util.getUnorderedEvaluationSet(evalNodes, terminatingNodes);
+    var orderedEvaluationSet = graph_util.getOrderedEvaluationSet(unorderedEvaluationSet);
+    return orderedEvaluationSet;
+}
+exports.getOrderedEvaluationSetFromEvalTensor = getOrderedEvaluationSetFromEvalTensor;
+function addPersistentArraysToTensorArrayMap(evaluationSet, tensorArrayMap) {
+    evaluationSet.forEach(function (node) {
+        if (node instanceof graph_1.VariableNode || node instanceof graph_1.ConstantNode) {
+            tensorArrayMap.set(node.output, node.data);
+        }
+    });
+}
+exports.addPersistentArraysToTensorArrayMap = addPersistentArraysToTensorArrayMap;
+function getVariableNodesFromEvaluationSet(evaluationSet) {
+    var nodes = [];
+    evaluationSet.forEach(function (node) {
+        if (node instanceof graph_1.VariableNode) {
+            nodes.push(node);
+        }
+    });
+    return nodes;
+}
+exports.getVariableNodesFromEvaluationSet = getVariableNodesFromEvaluationSet;
+function throwIfFeedDictionaryContainsNDArrays(feedDictionary) {
+    Object.keys(feedDictionary.dict).forEach(function (tensorID) {
+        if (feedDictionary.dict[+tensorID].data instanceof ndarray_1.NDArray) {
+            throw new Error('training requires FeedDictionary entries to be InputProviders' +
+                'and not NDArrays.');
+        }
+    });
+}
+exports.throwIfFeedDictionaryContainsNDArrays = throwIfFeedDictionaryContainsNDArrays;
+function loadInputsFromFeedDictionaryToTensorArrayMap(batchFeed, activations, math) {
+    Object.keys(batchFeed.dict).forEach(function (tensorID) {
+        var feedEntry = batchFeed.dict[+tensorID];
+        var data;
+        if (feedEntry.data instanceof ndarray_1.NDArray) {
+            data = feedEntry.data;
+        }
+        else {
+            var provider = feedEntry.data;
+            data = provider.getNextCopy(math);
+        }
+        util.assert(util.arraysEqual(feedEntry.tensor.shape, data.shape), "Error loading FeedEntry: feeding NDArray of shape " + data.shape + " " +
+            ("does not match Tensor (id: " + feedEntry.tensor.id + ") shape: ") +
+            (feedEntry.tensor.shape + "."));
+        activations.set(feedEntry.tensor, data);
+    });
+}
+exports.loadInputsFromFeedDictionaryToTensorArrayMap = loadInputsFromFeedDictionaryToTensorArrayMap;
+function releaseFeedDictionaryInputsFromTensorArrayMap(batchFeed, activations, math) {
+    Object.keys(batchFeed.dict).forEach(function (tensorID) {
+        var feedEntry = batchFeed.dict[+tensorID];
+        if (!(feedEntry.data instanceof ndarray_1.NDArray)) {
+            var provider = feedEntry.data;
+            var feedEntryArray = activations.get(feedEntry.tensor);
+            provider.disposeCopy(math, feedEntryArray);
+        }
+        activations.delete(feedEntry.tensor);
+    });
+}
+exports.releaseFeedDictionaryInputsFromTensorArrayMap = releaseFeedDictionaryInputsFromTensorArrayMap;
+function removeFeedDictionaryNodesFromEvaluationSet(feedDictionary, evaluationSet) {
+    var i = 0;
+    while (i < evaluationSet.length) {
+        var node = evaluationSet[i];
+        if (feedDictionary.dict[node.output.id] != null) {
+            evaluationSet.splice(i, 1);
+        }
+        else {
+            ++i;
+        }
+    }
+}
+exports.removeFeedDictionaryNodesFromEvaluationSet = removeFeedDictionaryNodesFromEvaluationSet;
+function disposeAndInitializeOperationOutputs(evaluationSet, tensorArrayMap) {
+    evaluationSet.forEach(function (node) {
+        if (!graph_util.isInputNode(node)) {
+            if (!graph_util.isPassthroughNode(node, tensorArrayMap)) {
+                tensorArrayMap.disposeArray(node.output);
+            }
+            tensorArrayMap.set(node.output, null);
+        }
+    });
+}
+exports.disposeAndInitializeOperationOutputs = disposeAndInitializeOperationOutputs;
+function disposeAndInitializeOperationInputGradients(evaluationSet, gradients) {
+    evaluationSet.forEach(function (node) {
+        Object.keys(node.inputs).forEach(function (inputName) {
+            var input = node.inputs[inputName];
+            if (gradients.get(input, true) !== gradients.get(node.output, true)) {
+                gradients.disposeArray(input);
+            }
+            gradients.nullify(input);
+        });
+    });
+}
+exports.disposeAndInitializeOperationInputGradients = disposeAndInitializeOperationInputGradients;
+function disposeTransientOperationArrays(operations, activations, gradients) {
+    operations.forEach(function (op) { return op.disposeTransientArrays(activations, gradients); });
+}
+exports.disposeTransientOperationArrays = disposeTransientOperationArrays;
+function throwErrorIfEvaluationSetContainsPlaceholderNodes(evaluationSet) {
+    evaluationSet.forEach(function (node) {
+        if (node instanceof graph_1.PlaceholderNode) {
+            var shape = '[' + node.output.shape.join(', ') + ']';
+            throw new Error('Placeholder node "' + node.name + '" ' + shape +
+                ' not present in feed dictionary.');
+        }
+    });
+}
+exports.throwErrorIfEvaluationSetContainsPlaceholderNodes = throwErrorIfEvaluationSetContainsPlaceholderNodes;
+
+},{"../math/ndarray":95,"../util":101,"./graph":16,"./graph_util":17}],49:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+var TensorArrayMapBase = (function () {
+    function TensorArrayMapBase() {
+        this.dict = {};
+    }
+    TensorArrayMapBase.prototype.get = function (tensor, skipChecks) {
+        if (skipChecks === void 0) { skipChecks = false; }
+        if (!skipChecks && this.dict[tensor.id] === undefined) {
+            throw new Error("tensor " + tensor.id + " not in array map.");
+        }
+        var nda = this.dict[tensor.id];
+        if (!skipChecks && nda === null) {
+            throw new Error("tensor " + tensor.id + " has null array.");
+        }
+        return nda;
+    };
+    TensorArrayMapBase.prototype.delete = function (tensor) {
+        delete this.dict[tensor.id];
+    };
+    TensorArrayMapBase.prototype.nullify = function (tensor) {
+        this.dict[tensor.id] = null;
+    };
+    TensorArrayMapBase.prototype.disposeArray = function (tensor) {
+        if (this.dict[tensor.id] === undefined) {
+            return;
+        }
+        var nda = this.dict[tensor.id];
+        if (nda === null) {
+            return;
+        }
+        nda.dispose();
+        this.dict[tensor.id] = null;
+    };
+    TensorArrayMapBase.prototype.size = function () {
+        return Object.keys(this.dict).length;
+    };
+    TensorArrayMapBase.prototype.dispose = function () {
+        var _this = this;
+        Object.keys(this.dict).forEach(function (tensorID) {
+            var nda = _this.dict[+tensorID];
+            if (nda) {
+                nda.dispose();
+            }
+        });
+        this.dict = {};
+    };
+    TensorArrayMapBase.prototype.hasNullArray = function (tensor) {
+        if (this.dict[tensor.id] === undefined) {
+            throw new Error("tensor " + tensor.id + " not in array map.");
+        }
+        return this.dict[tensor.id] === null;
+    };
+    return TensorArrayMapBase;
+}());
+exports.TensorArrayMapBase = TensorArrayMapBase;
+var TensorArrayMap = (function (_super) {
+    __extends(TensorArrayMap, _super);
+    function TensorArrayMap() {
+        return _super !== null && _super.apply(this, arguments) || this;
+    }
+    TensorArrayMap.prototype.set = function (tensor, array) {
+        this.dict[tensor.id] = array;
+    };
+    return TensorArrayMap;
+}(TensorArrayMapBase));
+exports.TensorArrayMap = TensorArrayMap;
+var SummedTensorArrayMap = (function (_super) {
+    __extends(SummedTensorArrayMap, _super);
+    function SummedTensorArrayMap(math) {
+        var _this = _super.call(this) || this;
+        _this.math = math;
+        return _this;
+    }
+    SummedTensorArrayMap.prototype.add = function (tensor, array) {
+        if (this.dict[tensor.id] == null) {
+            this.dict[tensor.id] = this.math.keep(array);
+        }
+        else {
+            var oldValue = this.get(tensor);
+            var newValue = this.math.keep(this.math.addStrict(oldValue, array));
+            this.dict[tensor.id] = newValue;
+            oldValue.dispose();
+        }
+    };
+    return SummedTensorArrayMap;
+}(TensorArrayMapBase));
+exports.SummedTensorArrayMap = SummedTensorArrayMap;
+
+},{}],50:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var session_1 = require("./graph/session");
+var ndarray_1 = require("./math/ndarray");
+var DEFAULT_EVAL_INTERVAL_MS = 1500;
+var DEFAULT_COST_INTERVAL_MS = 500;
+var DEFAULT_INFERENCE_EXAMPLE_INTERVAL_MS = 3000;
+var MetricReduction;
+(function (MetricReduction) {
+    MetricReduction[MetricReduction["SUM"] = 0] = "SUM";
+    MetricReduction[MetricReduction["MEAN"] = 1] = "MEAN";
+})(MetricReduction = exports.MetricReduction || (exports.MetricReduction = {}));
+var GraphRunner = (function () {
+    function GraphRunner(math, session, eventObserver) {
+        this.math = math;
+        this.session = session;
+        this.eventObserver = eventObserver;
+        this.lastCostTimestamp = 0;
+        this.lastEvalTimestamp = 0;
+        this.resetStatistics();
+        this.zeroScalar = ndarray_1.Scalar.new(0);
+    }
+    GraphRunner.prototype.resetStatistics = function () {
+        this.totalBatchesTrained = 0;
+    };
+    GraphRunner.prototype.train = function (costTensor, trainFeedEntries, batchSize, optimizer, numBatches, metricTensor, metricFeedEntries, metricBatchSize, metricReduction, evalIntervalMs, costIntervalMs) {
+        if (metricReduction === void 0) { metricReduction = MetricReduction.MEAN; }
+        if (evalIntervalMs === void 0) { evalIntervalMs = DEFAULT_EVAL_INTERVAL_MS; }
+        if (costIntervalMs === void 0) { costIntervalMs = DEFAULT_COST_INTERVAL_MS; }
+        this.costTensor = costTensor;
+        this.trainFeedEntries = trainFeedEntries;
+        this.metricTensor = metricTensor;
+        this.metricFeedEntries = metricFeedEntries;
+        if (metricBatchSize != null && this.metricBatchSize !== metricBatchSize) {
+            if (this.metricBatchSizeScalar != null) {
+                this.metricBatchSizeScalar.dispose();
+            }
+            this.metricBatchSizeScalar = ndarray_1.Scalar.new(metricBatchSize);
+        }
+        this.metricBatchSize = metricBatchSize;
+        this.metricReduction = metricReduction;
+        this.batchSize = batchSize;
+        this.optimizer = optimizer;
+        this.metricIntervalMs = evalIntervalMs;
+        this.costIntervalMs = costIntervalMs;
+        this.currentTrainLoopNumBatches = numBatches;
+        this.batchesTrainedThisRun = 0;
+        this.isTraining = true;
+        this.trainStartTimestamp = performance.now();
+        this.trainNetwork();
+    };
+    GraphRunner.prototype.stopTraining = function () {
+        this.isTraining = false;
+    };
+    GraphRunner.prototype.resumeTraining = function () {
+        this.isTraining = true;
+        this.trainNetwork();
+    };
+    GraphRunner.prototype.trainNetwork = function () {
+        var _this = this;
+        if (this.batchesTrainedThisRun === this.currentTrainLoopNumBatches) {
+            this.stopTraining();
+        }
+        if (!this.isTraining) {
+            if (this.eventObserver.doneTrainingCallback != null) {
+                this.eventObserver.doneTrainingCallback();
+            }
+            return;
+        }
+        var start = performance.now();
+        var shouldComputeCost = this.eventObserver.avgCostCallback != null &&
+            (start - this.lastCostTimestamp > this.costIntervalMs);
+        if (shouldComputeCost) {
+            this.lastCostTimestamp = start;
+        }
+        var costReduction = shouldComputeCost ? session_1.CostReduction.MEAN : session_1.CostReduction.NONE;
+        this.math.scope(function (keep) {
+            var avgCost = _this.session.train(_this.costTensor, _this.trainFeedEntries, _this.batchSize, _this.optimizer, costReduction);
+            if (shouldComputeCost) {
+                var trainTime = performance.now() - start;
+                _this.eventObserver.avgCostCallback(avgCost);
+                if (_this.eventObserver.trainExamplesPerSecCallback != null) {
+                    var examplesPerSec = (_this.batchSize * 1000 / trainTime);
+                    _this.eventObserver.trainExamplesPerSecCallback(examplesPerSec);
+                }
+            }
+            if (_this.eventObserver.metricCallback != null &&
+                _this.metricFeedEntries != null &&
+                start - _this.lastEvalTimestamp > _this.metricIntervalMs) {
+                _this.lastEvalTimestamp = start;
+                if (_this.lastComputedMetric != null) {
+                    _this.lastComputedMetric.dispose();
+                }
+                _this.lastComputedMetric = _this.computeMetric();
+                _this.eventObserver.metricCallback(_this.lastComputedMetric);
+            }
+            if (_this.eventObserver.totalTimeCallback != null) {
+                _this.eventObserver.totalTimeCallback((start - _this.trainStartTimestamp) / 1000);
+            }
+            _this.batchesTrainedThisRun++;
+            _this.totalBatchesTrained++;
+            if (_this.eventObserver.batchesTrainedCallback != null) {
+                _this.eventObserver.batchesTrainedCallback(_this.totalBatchesTrained);
+            }
+        });
+        requestAnimationFrame(function () { return _this.trainNetwork(); });
+    };
+    GraphRunner.prototype.infer = function (inferenceTensor, inferenceFeedEntries, inferenceExampleIntervalMs, inferenceExampleCount, numPasses) {
+        var _this = this;
+        if (inferenceExampleIntervalMs === void 0) { inferenceExampleIntervalMs = DEFAULT_INFERENCE_EXAMPLE_INTERVAL_MS; }
+        if (inferenceExampleCount === void 0) { inferenceExampleCount = 5; }
+        if (this.eventObserver.inferenceExamplesCallback == null &&
+            this.eventObserver.inferenceExamplesPerSecCallback == null) {
+            throw new Error('Cannot start inference loop, no inference example or ' +
+                'examples/sec observer provided.');
+        }
+        for (var i = 0; i < inferenceFeedEntries.length; i++) {
+            var feedEntry = inferenceFeedEntries[i];
+            if (feedEntry.data instanceof ndarray_1.NDArray) {
+                throw new Error('Cannot start inference on the model runner with feed entries of ' +
+                    'type NDArray. Please use InputProviders.');
+            }
+        }
+        this.inferenceExampleIntervalMs = inferenceExampleIntervalMs;
+        this.inferenceTensor = inferenceTensor;
+        this.inferenceFeedEntries = inferenceFeedEntries;
+        this.inferenceExampleCount = inferenceExampleCount;
+        this.currentInferenceLoopNumPasses = numPasses;
+        if (!this.isInferring) {
+            this.inferencePassesThisRun = 0;
+            requestAnimationFrame(function () { return _this.inferNetwork(); });
+        }
+        this.isInferring = true;
+    };
+    GraphRunner.prototype.inferNetwork = function () {
+        var _this = this;
+        if (!this.isInferring ||
+            this.inferencePassesThisRun === this.currentInferenceLoopNumPasses) {
+            return;
+        }
+        this.math.scope(function (keep) {
+            var feeds = [];
+            var inferenceValues = [];
+            var start = performance.now();
+            for (var i = 0; i < _this.inferenceExampleCount; i++) {
+                var ndarrayFeedEntries = [];
+                for (var j = 0; j < _this.inferenceFeedEntries.length; j++) {
+                    var feedEntry = _this.inferenceFeedEntries[j];
+                    var nextCopy = feedEntry.data.getNextCopy(_this.math);
+                    ndarrayFeedEntries.push({ tensor: feedEntry.tensor, data: nextCopy });
+                }
+                feeds.push(ndarrayFeedEntries);
+                inferenceValues.push(_this.session.eval(_this.inferenceTensor, ndarrayFeedEntries));
+            }
+            if (_this.eventObserver.inferenceExamplesPerSecCallback != null) {
+                inferenceValues[inferenceValues.length - 1].getValues();
+                var inferenceExamplesPerSecTime = performance.now() - start;
+                var examplesPerSec = (_this.inferenceExampleCount * 1000 / inferenceExamplesPerSecTime);
+                _this.eventObserver.inferenceExamplesPerSecCallback(examplesPerSec);
+            }
+            if (_this.eventObserver.inferenceExamplesCallback != null) {
+                _this.eventObserver.inferenceExamplesCallback(feeds, inferenceValues);
+            }
+            _this.inferencePassesThisRun++;
+        });
+        this.lastInferTimeoutID = window.setTimeout(function () { return _this.inferNetwork(); }, this.inferenceExampleIntervalMs);
+    };
+    GraphRunner.prototype.stopInferring = function () {
+        this.isInferring = false;
+        window.clearTimeout(this.lastInferTimeoutID);
+    };
+    GraphRunner.prototype.isInferenceRunning = function () {
+        return this.isInferring;
+    };
+    GraphRunner.prototype.computeMetric = function () {
+        var _this = this;
+        if (this.metricFeedEntries == null) {
+            throw new Error('Cannot compute metric, no metric FeedEntries provided.');
+        }
+        var metric = this.zeroScalar;
+        return this.math.scope(function (keep) {
+            for (var i = 0; i < _this.metricBatchSize; i++) {
+                var metricValue = _this.session.eval(_this.metricTensor, _this.metricFeedEntries);
+                metric = _this.math.add(metric, metricValue);
+            }
+            if (_this.metricReduction === MetricReduction.MEAN) {
+                metric = _this.math.divide(metric, _this.metricBatchSizeScalar);
+            }
+            return metric;
+        });
+    };
+    GraphRunner.prototype.getTotalBatchesTrained = function () {
+        return this.totalBatchesTrained;
+    };
+    GraphRunner.prototype.getLastComputedMetric = function () {
+        return this.lastComputedMetric;
+    };
+    GraphRunner.prototype.setMath = function (math) {
+        this.math = math;
+    };
+    GraphRunner.prototype.setSession = function (session) {
+        this.session = session;
+    };
+    GraphRunner.prototype.setInferenceTensor = function (inferenceTensor) {
+        this.inferenceTensor = inferenceTensor;
+    };
+    GraphRunner.prototype.setInferenceExampleCount = function (inferenceExampleCount) {
+        this.inferenceExampleCount = inferenceExampleCount;
+    };
+    return GraphRunner;
+}());
+exports.GraphRunner = GraphRunner;
+
+},{"./graph/session":47,"./math/ndarray":95}],51:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var xhr_dataset = require("./data/xhr-dataset");
+exports.xhr_dataset = xhr_dataset;
+var environment = require("./environment");
+exports.environment = environment;
+var gpgpu_util = require("./math/backends/webgl/gpgpu_util");
+exports.gpgpu_util = gpgpu_util;
+var render_ndarray_gpu_util = require("./math/backends/webgl/render_ndarray_gpu_util");
+exports.render_ndarray_gpu_util = render_ndarray_gpu_util;
+var webgl_util = require("./math/backends/webgl/webgl_util");
+exports.webgl_util = webgl_util;
+var conv_util = require("./math/conv_util");
+exports.conv_util = conv_util;
+var test_util = require("./test_util");
+exports.test_util = test_util;
+var util = require("./util");
+exports.util = util;
+var version_1 = require("./version");
+exports.version = version_1.version;
+var checkpoint_loader_1 = require("./data/checkpoint_loader");
+exports.CheckpointLoader = checkpoint_loader_1.CheckpointLoader;
+var dataset_1 = require("./data/dataset");
+exports.InMemoryDataset = dataset_1.InMemoryDataset;
+var input_provider_1 = require("./data/input_provider");
+exports.InCPUMemoryShuffledInputProviderBuilder = input_provider_1.InCPUMemoryShuffledInputProviderBuilder;
+exports.InGPUMemoryShuffledInputProviderBuilder = input_provider_1.InGPUMemoryShuffledInputProviderBuilder;
+var xhr_dataset_1 = require("./data/xhr-dataset");
+exports.XhrDataset = xhr_dataset_1.XhrDataset;
+var environment_1 = require("./environment");
+exports.ENV = environment_1.ENV;
+exports.Environment = environment_1.Environment;
+var graph_1 = require("./graph/graph");
+exports.Graph = graph_1.Graph;
+exports.Tensor = graph_1.Tensor;
+var adadelta_optimizer_1 = require("./graph/optimizers/adadelta_optimizer");
+exports.AdadeltaOptimizer = adadelta_optimizer_1.AdadeltaOptimizer;
+var adagrad_optimizer_1 = require("./graph/optimizers/adagrad_optimizer");
+exports.AdagradOptimizer = adagrad_optimizer_1.AdagradOptimizer;
+var adam_optimizer_1 = require("./graph/optimizers/adam_optimizer");
+exports.AdamOptimizer = adam_optimizer_1.AdamOptimizer;
+var adamax_optimizer_1 = require("./graph/optimizers/adamax_optimizer");
+exports.AdamaxOptimizer = adamax_optimizer_1.AdamaxOptimizer;
+var momentum_optimizer_1 = require("./graph/optimizers/momentum_optimizer");
+exports.MomentumOptimizer = momentum_optimizer_1.MomentumOptimizer;
+var optimizer_1 = require("./graph/optimizers/optimizer");
+exports.Optimizer = optimizer_1.Optimizer;
+var rmsprop_optimizer_1 = require("./graph/optimizers/rmsprop_optimizer");
+exports.RMSPropOptimizer = rmsprop_optimizer_1.RMSPropOptimizer;
+var sgd_optimizer_1 = require("./graph/optimizers/sgd_optimizer");
+exports.SGDOptimizer = sgd_optimizer_1.SGDOptimizer;
+var session_1 = require("./graph/session");
+exports.CostReduction = session_1.CostReduction;
+exports.Session = session_1.Session;
+var graph_runner_1 = require("./graph_runner");
+exports.GraphRunner = graph_runner_1.GraphRunner;
+exports.MetricReduction = graph_runner_1.MetricReduction;
+var initializers_1 = require("./initializers");
+exports.ConstantInitializer = initializers_1.ConstantInitializer;
+exports.NDArrayInitializer = initializers_1.NDArrayInitializer;
+exports.OnesInitializer = initializers_1.OnesInitializer;
+exports.RandomNormalInitializer = initializers_1.RandomNormalInitializer;
+exports.RandomTruncatedNormalInitializer = initializers_1.RandomTruncatedNormalInitializer;
+exports.RandomUniformInitializer = initializers_1.RandomUniformInitializer;
+exports.VarianceScalingInitializer = initializers_1.VarianceScalingInitializer;
+exports.ZerosInitializer = initializers_1.ZerosInitializer;
+var backend_cpu_1 = require("./math/backends/backend_cpu");
+exports.MathBackendCPU = backend_cpu_1.MathBackendCPU;
+exports.NDArrayMathCPU = backend_cpu_1.NDArrayMathCPU;
+var backend_webgl_1 = require("./math/backends/backend_webgl");
+exports.MathBackendWebGL = backend_webgl_1.MathBackendWebGL;
+exports.NDArrayMathGPU = backend_webgl_1.NDArrayMathGPU;
+var matmul_1 = require("./math/backends/types/matmul");
+exports.MatrixOrientation = matmul_1.MatrixOrientation;
+var gpgpu_context_1 = require("./math/backends/webgl/gpgpu_context");
+exports.GPGPUContext = gpgpu_context_1.GPGPUContext;
+var math_1 = require("./math/math");
+exports.NDArrayMath = math_1.NDArrayMath;
+var ndarray_1 = require("./math/ndarray");
+exports.Array1D = ndarray_1.Array1D;
+exports.Array2D = ndarray_1.Array2D;
+exports.Array3D = ndarray_1.Array3D;
+exports.Array4D = ndarray_1.Array4D;
+exports.NDArray = ndarray_1.NDArray;
+exports.Scalar = ndarray_1.Scalar;
+
+},{"./data/checkpoint_loader":10,"./data/dataset":11,"./data/input_provider":12,"./data/xhr-dataset":13,"./environment":15,"./graph/graph":16,"./graph/optimizers/adadelta_optimizer":38,"./graph/optimizers/adagrad_optimizer":39,"./graph/optimizers/adam_optimizer":40,"./graph/optimizers/adamax_optimizer":41,"./graph/optimizers/momentum_optimizer":42,"./graph/optimizers/optimizer":43,"./graph/optimizers/rmsprop_optimizer":44,"./graph/optimizers/sgd_optimizer":45,"./graph/session":47,"./graph_runner":50,"./initializers":52,"./math/backends/backend_cpu":55,"./math/backends/backend_webgl":57,"./math/backends/types/matmul":61,"./math/backends/webgl/gpgpu_context":71,"./math/backends/webgl/gpgpu_util":73,"./math/backends/webgl/render_ndarray_gpu_util":80,"./math/backends/webgl/webgl_util":89,"./math/conv_util":92,"./math/math":94,"./math/ndarray":95,"./test_util":100,"./util":101,"./version":102}],52:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("./math/ndarray");
+var VarianceScalingInitializer = (function () {
+    function VarianceScalingInitializer(scale, mode, distribution) {
+        if (scale === void 0) { scale = 1.0; }
+        if (mode === void 0) { mode = 'fan_in'; }
+        if (distribution === void 0) { distribution = 'normal'; }
+        this.scale = scale;
+        this.mode = mode;
+        this.distribution = distribution;
+    }
+    VarianceScalingInitializer.prototype.initialize = function (weightsShape, inputUnits, outputUnits) {
+        var n = 0;
+        if (this.mode === 'fan_in') {
+            n = inputUnits;
+        }
+        else if (this.mode === 'fan_out') {
+            n = outputUnits;
+        }
+        else if (this.mode === 'fan_avg') {
+            n = (inputUnits + outputUnits) / 2;
+        }
+        else {
+            throw new Error("Unexpected mode for variance scaling initializer: " + this.mode);
+        }
+        if (this.distribution === 'normal') {
+            return ndarray_1.NDArray.randTruncatedNormal(weightsShape, 0.0, Math.sqrt(this.scale / n));
+        }
+        else if (this.distribution === 'uniform') {
+            return ndarray_1.NDArray.randUniform(weightsShape, 0.0, Math.sqrt(3 * this.scale / n));
+        }
+        else {
+            throw new Error("Unexpected distribution for variance scaling initializer: " +
+                ("" + this.distribution));
+        }
+    };
+    return VarianceScalingInitializer;
+}());
+exports.VarianceScalingInitializer = VarianceScalingInitializer;
+var ZerosInitializer = (function () {
+    function ZerosInitializer() {
+    }
+    ZerosInitializer.prototype.initialize = function (weightsShape, inputUnits, outputUnits) {
+        return ndarray_1.NDArray.zeros(weightsShape);
+    };
+    return ZerosInitializer;
+}());
+exports.ZerosInitializer = ZerosInitializer;
+var OnesInitializer = (function () {
+    function OnesInitializer() {
+    }
+    OnesInitializer.prototype.initialize = function (weightsShape, inputUnits, outputUnits) {
+        var values = ndarray_1.NDArray.zeros(weightsShape);
+        values.fill(1);
+        return values;
+    };
+    return OnesInitializer;
+}());
+exports.OnesInitializer = OnesInitializer;
+var ConstantInitializer = (function () {
+    function ConstantInitializer(value) {
+        if (value === void 0) { value = 0; }
+        this.value = value;
+    }
+    ConstantInitializer.prototype.initialize = function (weightsShape, inputUnits, outputUnits) {
+        var values = ndarray_1.NDArray.zeros(weightsShape);
+        values.fill(this.value);
+        return values;
+    };
+    return ConstantInitializer;
+}());
+exports.ConstantInitializer = ConstantInitializer;
+var NDArrayInitializer = (function () {
+    function NDArrayInitializer(ndarray) {
+        this.ndarray = ndarray;
+    }
+    NDArrayInitializer.prototype.initialize = function (weightsShape, inputUnits, outputUnits) {
+        return this.ndarray;
+    };
+    return NDArrayInitializer;
+}());
+exports.NDArrayInitializer = NDArrayInitializer;
+var RandomNormalInitializer = (function () {
+    function RandomNormalInitializer(mean, stdev) {
+        if (mean === void 0) { mean = 0; }
+        if (stdev === void 0) { stdev = .05; }
+        this.mean = mean;
+        this.stdev = stdev;
+    }
+    RandomNormalInitializer.prototype.initialize = function (weightsShape, inputUnits, outputUnits) {
+        return ndarray_1.NDArray.randNormal(weightsShape, this.mean, this.stdev);
+    };
+    return RandomNormalInitializer;
+}());
+exports.RandomNormalInitializer = RandomNormalInitializer;
+var RandomTruncatedNormalInitializer = (function () {
+    function RandomTruncatedNormalInitializer(mean, stdev) {
+        if (mean === void 0) { mean = 0; }
+        if (stdev === void 0) { stdev = .05; }
+        this.mean = mean;
+        this.stdev = stdev;
+    }
+    RandomTruncatedNormalInitializer.prototype.initialize = function (weightsShape, inputUnits, outputUnits) {
+        return ndarray_1.NDArray.randTruncatedNormal(weightsShape, this.mean, this.stdev);
+    };
+    return RandomTruncatedNormalInitializer;
+}());
+exports.RandomTruncatedNormalInitializer = RandomTruncatedNormalInitializer;
+var RandomUniformInitializer = (function () {
+    function RandomUniformInitializer(minval, maxval) {
+        if (minval === void 0) { minval = -.05; }
+        if (maxval === void 0) { maxval = .05; }
+        this.minval = minval;
+        this.maxval = maxval;
+    }
+    RandomUniformInitializer.prototype.initialize = function (weightsShape, inputUnits, outputUnits) {
+        return ndarray_1.NDArray.randUniform(weightsShape, this.minval, this.maxval);
+    };
+    return RandomUniformInitializer;
+}());
+exports.RandomUniformInitializer = RandomUniformInitializer;
+
+},{"./math/ndarray":95}],53:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("./ndarray");
+var TanHFunc = (function () {
+    function TanHFunc() {
+        this.one = ndarray_1.Scalar.new(1);
+    }
+    TanHFunc.prototype.output = function (math, x) {
+        return math.tanh(x);
+    };
+    TanHFunc.prototype.der = function (math, x, y) {
+        var _this = this;
+        return math.scope(function () {
+            var ySquared = math.elementWiseMul(y, y);
+            return math.scalarMinusArray(_this.one, ySquared);
+        });
+    };
+    TanHFunc.prototype.dispose = function () {
+        this.one.dispose();
+    };
+    return TanHFunc;
+}());
+exports.TanHFunc = TanHFunc;
+var ReLUFunc = (function () {
+    function ReLUFunc() {
+    }
+    ReLUFunc.prototype.output = function (math, x) {
+        return math.relu(x);
+    };
+    ReLUFunc.prototype.der = function (math, x, y) {
+        return math.step(x);
+    };
+    ReLUFunc.prototype.dispose = function () { };
+    return ReLUFunc;
+}());
+exports.ReLUFunc = ReLUFunc;
+var LeakyReluFunc = (function () {
+    function LeakyReluFunc(alpha) {
+        this.alpha = alpha;
+    }
+    LeakyReluFunc.prototype.output = function (math, x) {
+        return math.leakyRelu(x, this.alpha);
+    };
+    LeakyReluFunc.prototype.der = function (math, x, y) {
+        return math.step(x, this.alpha);
+    };
+    LeakyReluFunc.prototype.dispose = function () { };
+    return LeakyReluFunc;
+}());
+exports.LeakyReluFunc = LeakyReluFunc;
+var SigmoidFunc = (function () {
+    function SigmoidFunc() {
+    }
+    SigmoidFunc.prototype.output = function (math, x) {
+        return math.sigmoid(x);
+    };
+    SigmoidFunc.prototype.der = function (math, x, y) {
+        return math.scope(function () {
+            var ySquared = math.elementWiseMul(y, y);
+            return math.subStrict(y, ySquared);
+        });
+    };
+    SigmoidFunc.prototype.dispose = function () { };
+    return SigmoidFunc;
+}());
+exports.SigmoidFunc = SigmoidFunc;
+var SquareFunc = (function () {
+    function SquareFunc() {
+        this.two = ndarray_1.Scalar.new(2);
+    }
+    SquareFunc.prototype.output = function (math, x) {
+        return math.elementWiseMul(x, x);
+    };
+    SquareFunc.prototype.der = function (math, x, y) {
+        return math.scalarTimesArray(this.two, x);
+    };
+    SquareFunc.prototype.dispose = function () {
+        this.two.dispose();
+    };
+    return SquareFunc;
+}());
+exports.SquareFunc = SquareFunc;
+var EluFunc = (function () {
+    function EluFunc() {
+    }
+    EluFunc.prototype.output = function (math, x) {
+        return math.elu(x);
+    };
+    EluFunc.prototype.der = function (math, x, y) {
+        return math.eluDer(x);
+    };
+    EluFunc.prototype.dispose = function () { };
+    return EluFunc;
+}());
+exports.EluFunc = EluFunc;
+
+},{"./ndarray":95}],54:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+function axesAreInnerMostDims(axes, rank) {
+    for (var i = 0; i < axes.length; ++i) {
+        if (axes[axes.length - i - 1] !== rank - 1 - i) {
+            return false;
+        }
+    }
+    return true;
+}
+exports.axesAreInnerMostDims = axesAreInnerMostDims;
+function combineLocations(outputLoc, reduceLoc, axes) {
+    var rank = outputLoc.length + reduceLoc.length;
+    var loc = [];
+    var outIdx = 0;
+    var reduceIdx = 0;
+    for (var dim = 0; dim < rank; dim++) {
+        if (axes.indexOf(dim) === -1) {
+            loc.push(outputLoc[outIdx++]);
+        }
+        else {
+            loc.push(reduceLoc[reduceIdx++]);
+        }
+    }
+    return loc;
+}
+exports.combineLocations = combineLocations;
+function computeOutAndReduceShapes(aShape, axes) {
+    var outShape = [];
+    var rank = aShape.length;
+    for (var dim = 0; dim < rank; dim++) {
+        if (axes.indexOf(dim) === -1) {
+            outShape.push(aShape[dim]);
+        }
+    }
+    var reduceShape = axes.map(function (dim) { return aShape[dim]; });
+    return [outShape, reduceShape];
+}
+exports.computeOutAndReduceShapes = computeOutAndReduceShapes;
+function expandShapeToKeepDim(shape, axes) {
+    var reduceSubShape = axes.map(function (x) { return 1; });
+    return combineLocations(shape, reduceSubShape, axes);
+}
+exports.expandShapeToKeepDim = expandShapeToKeepDim;
+function parseAxisParam(axis, shape) {
+    if (axis == null) {
+        axis = shape.map(function (s, i) { return i; });
+    }
+    else if (typeof (axis) === 'number') {
+        axis = [axis];
+    }
+    return axis;
+}
+exports.parseAxisParam = parseAxisParam;
+function assertAxesAreInnerMostDims(msg, axes, rank) {
+    if (!axesAreInnerMostDims(axes, rank)) {
+        throw new Error(msg + " supports only inner-most axes for now. " +
+            ("Got axes " + axes + " and rank-" + rank + " input."));
+    }
+}
+exports.assertAxesAreInnerMostDims = assertAxesAreInnerMostDims;
+function getPermutedAxes(axes, rank) {
+    if (axesAreInnerMostDims(axes, rank)) {
+        return null;
+    }
+    var result = [];
+    for (var i = 0; i < rank; ++i) {
+        if (axes.indexOf(i) === -1) {
+            result.push(i);
+        }
+    }
+    axes.forEach(function (axis) { return result.push(axis); });
+    return result;
+}
+exports.getPermutedAxes = getPermutedAxes;
+function getInnerMostAxes(numAxes, rank) {
+    var res = [];
+    for (var i = rank - numAxes; i < rank; ++i) {
+        res.push(i);
+    }
+    return res;
+}
+exports.getInnerMostAxes = getInnerMostAxes;
+
+},{}],55:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __generator = (this && this.__generator) || function (thisArg, body) {
+    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
+    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
+    function verb(n) { return function (v) { return step([n, v]); }; }
+    function step(op) {
+        if (f) throw new TypeError("Generator is already executing.");
+        while (_) try {
+            if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
+            if (y = 0, t) op = [0, t.value];
+            switch (op[0]) {
+                case 0: case 1: t = op; break;
+                case 4: _.label++; return { value: op[1], done: false };
+                case 5: _.label++; y = op[1]; op = [0]; continue;
+                case 7: op = _.ops.pop(); _.trys.pop(); continue;
+                default:
+                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
+                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
+                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
+                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
+                    if (t[2]) _.ops.pop();
+                    _.trys.pop(); continue;
+            }
+            op = body.call(thisArg, _);
+        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
+        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
+    }
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+var seedrandom = require("seedrandom");
+var environment_1 = require("../../environment");
+var util = require("../../util");
+var broadcast_util = require("../broadcast_util");
+var concat_util = require("../concat_util");
+var math_1 = require("../math");
+var ndarray_1 = require("../ndarray");
+var types = require("../types");
+var types_1 = require("../types");
+var axis_util = require("./../axis_util");
+var matmul_1 = require("./types/matmul");
+var MathBackendCPU = (function () {
+    function MathBackendCPU() {
+        this.data = {};
+    }
+    MathBackendCPU.prototype.dispose = function () { };
+    MathBackendCPU.prototype.write = function (id, values, dtype, shape) {
+        this.data[id] = values;
+    };
+    MathBackendCPU.prototype.writePixels = function (id, pixels, numChannels) {
+        var vals;
+        if (pixels instanceof ImageData) {
+            vals = pixels.data;
+        }
+        else if (pixels instanceof HTMLCanvasElement) {
+            vals = pixels.getContext('2d')
+                .getImageData(0, 0, pixels.width, pixels.height)
+                .data;
+        }
+        else if (pixels instanceof HTMLImageElement ||
+            pixels instanceof HTMLVideoElement) {
+            var canvas = document.createElement('canvas');
+            canvas.width = pixels.width;
+            canvas.height = pixels.height;
+            canvas.getContext('2d').drawImage(pixels, 0, 0, canvas.width, canvas.height);
+            vals = canvas.getContext('2d')
+                .getImageData(0, 0, canvas.width, canvas.height)
+                .data;
+        }
+        else {
+            throw new Error("pixels is of unknown type: " + pixels.constructor.name);
+        }
+        var values;
+        if (numChannels === 4) {
+            values = new Int32Array(vals);
+        }
+        else {
+            var numPixels = pixels.width * pixels.height;
+            values = new Int32Array(numPixels * numChannels);
+            for (var i = 0; i < numPixels; i++) {
+                for (var channel = 0; channel < numChannels; ++channel) {
+                    values[i * numChannels + channel] = vals[i * 4 + channel];
+                }
+            }
+        }
+        this.data[id] = values;
+    };
+    MathBackendCPU.prototype.read = function (id) {
+        return __awaiter(this, void 0, void 0, function () {
+            return __generator(this, function (_a) {
+                this.throwIfNoData(id);
+                return [2, this.data[id]];
+            });
+        });
+    };
+    MathBackendCPU.prototype.readSync = function (id) {
+        this.throwIfNoData(id);
+        return this.data[id];
+    };
+    MathBackendCPU.prototype.disposeData = function (id) {
+        delete this.data[id];
+    };
+    MathBackendCPU.prototype.time = function (query) {
+        return __awaiter(this, void 0, void 0, function () {
+            var start;
+            return __generator(this, function (_a) {
+                start = performance.now();
+                query();
+                return [2, performance.now() - start];
+            });
+        });
+    };
+    MathBackendCPU.prototype.throwIfNoData = function (id) {
+        if (!(id in this.data)) {
+            throw new Error("No data found for NDArray with id " + id + ". " +
+                "Use dl.ENV.math instead of constructing your own NDArrayMath. " +
+                "If you need to construct your own math, make sure this array is " +
+                "allocated after the math construction");
+        }
+    };
+    MathBackendCPU.prototype.clone = function (x) {
+        return ndarray_1.NDArray.make(x.shape, { values: new Float32Array(x.getValues()) });
+    };
+    MathBackendCPU.prototype.slice1D = function (x, begin, size) {
+        var newVals = x.getValues().slice(begin, begin + size);
+        return ndarray_1.Array1D.new(newVals);
+    };
+    MathBackendCPU.prototype.slice2D = function (x, begin, size) {
+        var result = ndarray_1.Array2D.zeros(size);
+        var startI = begin[0], startJ = begin[1];
+        for (var i = 0; i < size[0]; ++i) {
+            for (var j = 0; j < size[1]; ++j) {
+                var val = x.get(i + startI, j + startJ);
+                result.set(val, i, j);
+            }
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.slice3D = function (x, begin, size) {
+        var result = ndarray_1.Array3D.zeros(size);
+        var startI = begin[0], startJ = begin[1], startK = begin[2];
+        for (var i = 0; i < size[0]; ++i) {
+            for (var j = 0; j < size[1]; ++j) {
+                for (var k = 0; k < size[2]; ++k) {
+                    var val = x.get(i + startI, j + startJ, k + startK);
+                    result.set(val, i, j, k);
+                }
+            }
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.slice4D = function (x, begin, size) {
+        var result = ndarray_1.Array4D.zeros(size);
+        var startI = begin[0], startJ = begin[1], startK = begin[2], startL = begin[3];
+        for (var i = 0; i < size[0]; ++i) {
+            for (var j = 0; j < size[1]; ++j) {
+                for (var k = 0; k < size[2]; ++k) {
+                    for (var l = 0; l < size[3]; ++l) {
+                        var val = x.get(i + startI, j + startJ, k + startK, l + startL);
+                        result.set(val, i, j, k, l);
+                    }
+                }
+            }
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.concat1D = function (a, b) {
+        var outShape = concat_util.computeOutShape(a.shape, b.shape, 0);
+        var result = ndarray_1.Array1D.zeros(outShape);
+        var aVals = a.getValues();
+        var bVals = b.getValues();
+        var vals = result.getValues();
+        vals.set(aVals, 0);
+        vals.set(bVals, a.size);
+        return result;
+    };
+    MathBackendCPU.prototype.concat2D = function (a, b, axis) {
+        var outShape = concat_util.computeOutShape(a.shape, b.shape, axis);
+        var result = ndarray_1.Array2D.zeros(outShape);
+        if (axis === 0) {
+            var aVals = a.getValues();
+            var bVals = b.getValues();
+            var vals = result.getValues();
+            vals.set(aVals, 0);
+            vals.set(bVals, a.size);
+            return result;
+        }
+        for (var i = 0; i < outShape[0]; ++i) {
+            for (var j = 0; j < outShape[1]; ++j) {
+                var index = [i, j];
+                var value = void 0;
+                if (index[axis] < a.shape[axis]) {
+                    value = a.get(i, j);
+                }
+                else {
+                    index[axis] -= a.shape[axis];
+                    var i2 = index[0], j2 = index[1];
+                    value = b.get(i2, j2);
+                }
+                result.set(value, i, j);
+            }
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.concat3D = function (a, b, axis) {
+        var outShape = concat_util.computeOutShape(a.shape, b.shape, axis);
+        var result = ndarray_1.Array3D.zeros(outShape);
+        if (axis === 0) {
+            var aVals = a.getValues();
+            var bVals = b.getValues();
+            var vals = result.getValues();
+            vals.set(aVals, 0);
+            vals.set(bVals, a.size);
+            return result;
+        }
+        for (var i = 0; i < outShape[0]; ++i) {
+            for (var j = 0; j < outShape[1]; ++j) {
+                for (var k = 0; k < outShape[2]; ++k) {
+                    var index = [i, j, k];
+                    var value = void 0;
+                    if (index[axis] < a.shape[axis]) {
+                        value = a.get(i, j, k);
+                    }
+                    else {
+                        index[axis] -= a.shape[axis];
+                        var i2 = index[0], j2 = index[1], k2 = index[2];
+                        value = b.get(i2, j2, k2);
+                    }
+                    result.set(value, i, j, k);
+                }
+            }
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.concat4D = function (a, b, axis) {
+        var outShape = concat_util.computeOutShape(a.shape, b.shape, axis);
+        var result = ndarray_1.Array4D.zeros(outShape);
+        if (axis === 0) {
+            var aVals = a.getValues();
+            var bVals = b.getValues();
+            var vals = result.getValues();
+            vals.set(aVals, 0);
+            vals.set(bVals, a.size);
+            return result;
+        }
+        for (var i = 0; i < outShape[0]; ++i) {
+            for (var j = 0; j < outShape[1]; ++j) {
+                for (var k = 0; k < outShape[2]; ++k) {
+                    for (var l = 0; l < outShape[3]; ++l) {
+                        var index = [i, j, k, l];
+                        var value = void 0;
+                        if (index[axis] < a.shape[axis]) {
+                            value = a.get(i, j, k, l);
+                        }
+                        else {
+                            index[axis] -= a.shape[axis];
+                            var i2 = index[0], j2 = index[1], k2 = index[2], l2 = index[3];
+                            value = b.get(i2, j2, k2, l2);
+                        }
+                        result.set(value, i, j, k, l);
+                    }
+                }
+            }
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.neg = function (x) {
+        return this.multiply(ndarray_1.Scalar.new(-1), x);
+    };
+    MathBackendCPU.prototype.add = function (a, b) {
+        return this.broadcastedBinaryOp(a, b, types.upcastType(a.dtype, b.dtype), function (aValue, bValue) { return aValue + bValue; });
+    };
+    MathBackendCPU.prototype.subtract = function (a, b) {
+        return this.broadcastedBinaryOp(a, b, types.upcastType(a.dtype, b.dtype), function (aValue, bValue) { return aValue - bValue; });
+    };
+    MathBackendCPU.prototype.pow = function (a, b) {
+        return this.broadcastedBinaryOp(a, b, a.dtype, function (aValue, bValue) { return Math.pow(aValue, bValue); });
+    };
+    MathBackendCPU.prototype.matMul = function (a, b, aOrientation, bOrientation) {
+        if (aOrientation === void 0) { aOrientation = matmul_1.MatrixOrientation.REGULAR; }
+        if (bOrientation === void 0) { bOrientation = matmul_1.MatrixOrientation.REGULAR; }
+        var sharedDim = (aOrientation === matmul_1.MatrixOrientation.REGULAR) ? a.shape[1] : a.shape[0];
+        var leftDim = (aOrientation === matmul_1.MatrixOrientation.REGULAR) ? a.shape[0] : a.shape[1];
+        var rightDim = (bOrientation === matmul_1.MatrixOrientation.REGULAR) ? b.shape[1] : b.shape[0];
+        var normalGetter = function (matrix, i, j) {
+            return matrix.get(i, j);
+        };
+        var transposedGetter = function (matrix, i, j) {
+            return matrix.get(j, i);
+        };
+        var aGetter = (aOrientation === matmul_1.MatrixOrientation.REGULAR) ?
+            normalGetter :
+            transposedGetter;
+        var bGetter = (bOrientation === matmul_1.MatrixOrientation.REGULAR) ?
+            normalGetter :
+            transposedGetter;
+        var values = new Float32Array(leftDim * rightDim);
+        var index = 0;
+        for (var i = 0; i < leftDim; ++i) {
+            for (var j = 0; j < rightDim; ++j) {
+                var sum = 0;
+                for (var k = 0; k < sharedDim; ++k) {
+                    sum += aGetter(a, i, k) * bGetter(b, k, j);
+                }
+                values[index++] = sum;
+            }
+        }
+        return ndarray_1.Array2D.new([leftDim, rightDim], values);
+    };
+    MathBackendCPU.prototype.multiply = function (a, b) {
+        return this.broadcastedBinaryOp(a, b, a.dtype, function (aValue, bValue) { return aValue * bValue; });
+    };
+    MathBackendCPU.prototype.divide = function (a, b) {
+        return this.broadcastedBinaryOp(a, b, 'float32', function (aValue, bValue) { return aValue / bValue; });
+    };
+    MathBackendCPU.prototype.sum = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('sum', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var resultDtype = types_1.SumTypesMap[x.dtype];
+        var result = ndarray_1.NDArray.zeros(outShape, resultDtype);
+        var reduceSize = util.sizeFromShape(reduceShape);
+        var vals = result.getValues();
+        var aVals = x.getValues();
+        for (var i = 0; i < vals.length; ++i) {
+            var offset = i * reduceSize;
+            var sum = 0;
+            for (var j = 0; j < reduceSize; ++j) {
+                sum += aVals[offset + j];
+            }
+            vals[i] = sum;
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.argMin = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('argMin', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var result = ndarray_1.NDArray.zeros(outShape, 'int32');
+        var reduceSize = util.sizeFromShape(reduceShape);
+        var vals = result.getValues();
+        var aVals = x.getValues();
+        for (var i = 0; i < vals.length; ++i) {
+            var offset = i * reduceSize;
+            var min = aVals[offset];
+            var minIndex = 0;
+            for (var j = 0; j < reduceSize; ++j) {
+                var value = aVals[offset + j];
+                if (isNaN(value)) {
+                    minIndex = util.NAN_INT32;
+                    break;
+                }
+                if (value < min) {
+                    min = value;
+                    minIndex = j;
+                }
+            }
+            vals[i] = minIndex;
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.argMax = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('argMax', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var result = ndarray_1.NDArray.zeros(outShape, 'int32');
+        var reduceSize = util.sizeFromShape(reduceShape);
+        var vals = result.getValues();
+        var aVals = x.getValues();
+        for (var i = 0; i < vals.length; ++i) {
+            var offset = i * reduceSize;
+            var max = aVals[offset];
+            var maxIndex = 0;
+            for (var j = 0; j < reduceSize; ++j) {
+                var value = aVals[offset + j];
+                if (isNaN(value)) {
+                    maxIndex = util.NAN_INT32;
+                    break;
+                }
+                if (value > max) {
+                    max = value;
+                    maxIndex = j;
+                }
+            }
+            vals[i] = maxIndex;
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.equal = function (a, b) {
+        return this.broadcastedBinaryOp(a, b, 'bool', function (aVal, bVal) {
+            if (util.isValNaN(aVal, a.dtype) || util.isValNaN(bVal, b.dtype)) {
+                return util.getNaN('bool');
+            }
+            else {
+                return (aVal === bVal) ? 1 : 0;
+            }
+        });
+    };
+    MathBackendCPU.prototype.topKValues = function (x, k) {
+        return this.topK(x, k).values;
+    };
+    MathBackendCPU.prototype.topKIndices = function (x, k) {
+        return this.topK(x, k).indices;
+    };
+    MathBackendCPU.prototype.topK = function (x, k) {
+        var values = x.getValues();
+        var valuesAndIndices = [];
+        for (var i = 0; i < values.length; i++) {
+            valuesAndIndices.push({ value: values[i], index: i });
+        }
+        valuesAndIndices.sort(function (a, b) {
+            return b.value - a.value;
+        });
+        var topkValues = util.getTypedArrayFromDType(x.dtype, k);
+        var topkIndices = new Int32Array(k);
+        for (var i = 0; i < k; i++) {
+            topkValues[i] = valuesAndIndices[i].value;
+            topkIndices[i] = valuesAndIndices[i].index;
+        }
+        return {
+            values: ndarray_1.Array1D.new(topkValues),
+            indices: ndarray_1.Array1D.new(topkIndices)
+        };
+    };
+    MathBackendCPU.prototype.min = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('min', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var result = ndarray_1.NDArray.zeros(outShape, x.dtype);
+        var reduceSize = util.sizeFromShape(reduceShape);
+        var vals = result.getValues();
+        var aVals = x.getValues();
+        for (var i = 0; i < vals.length; ++i) {
+            var offset = i * reduceSize;
+            var min = aVals[0];
+            for (var j = 0; j < reduceSize; ++j) {
+                var value = aVals[offset + j];
+                if (isNaN(value)) {
+                    min = Number.NaN;
+                    break;
+                }
+                if (value < min) {
+                    min = value;
+                }
+            }
+            vals[i] = min;
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.max = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('max', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var result = ndarray_1.NDArray.zeros(outShape, x.dtype);
+        var reduceSize = util.sizeFromShape(reduceShape);
+        var vals = result.getValues();
+        var aVals = x.getValues();
+        for (var i = 0; i < vals.length; ++i) {
+            var offset = i * reduceSize;
+            var max = aVals[offset];
+            for (var j = 0; j < reduceSize; ++j) {
+                var value = aVals[offset + j];
+                if (isNaN(value)) {
+                    max = Number.NaN;
+                    break;
+                }
+                if (value > max) {
+                    max = value;
+                }
+            }
+            vals[i] = max;
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.ceil = function (x) {
+        var values = x.getValues();
+        var newValues = new Float32Array(values.length);
+        for (var i = 0; i < values.length; ++i) {
+            newValues[i] = Math.ceil(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: newValues });
+    };
+    MathBackendCPU.prototype.floor = function (x) {
+        var values = x.getValues();
+        var newValues = new Float32Array(values.length);
+        for (var i = 0; i < values.length; ++i) {
+            newValues[i] = Math.floor(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: newValues });
+    };
+    MathBackendCPU.prototype.exp = function (x) {
+        var values = x.getValues();
+        var newValues = new Float32Array(values.length);
+        for (var i = 0; i < values.length; ++i) {
+            newValues[i] = Math.exp(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: newValues });
+    };
+    MathBackendCPU.prototype.log = function (x) {
+        var values = x.getValues();
+        var newValues = new Float32Array(values.length);
+        for (var i = 0; i < values.length; ++i) {
+            var value = values[i];
+            newValues[i] = Math.log(value);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: newValues });
+    };
+    MathBackendCPU.prototype.sqrt = function (x) {
+        var values = x.getValues();
+        var newValues = new Float32Array(values.length);
+        for (var i = 0; i < values.length; ++i) {
+            var value = values[i];
+            newValues[i] = Math.sqrt(value);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: newValues });
+    };
+    MathBackendCPU.prototype.square = function (x) {
+        var values = x.getValues();
+        var newValues = new Float32Array(values.length);
+        for (var i = 0; i < values.length; ++i) {
+            var value = values[i];
+            newValues[i] = value * value;
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: newValues });
+    };
+    MathBackendCPU.prototype.relu = function (x) {
+        var res = ndarray_1.NDArray.zeros(x.shape, x.dtype);
+        var resVals = res.getValues();
+        var inVals = x.getValues();
+        for (var i = 0; i < inVals.length; ++i) {
+            var val = inVals[i];
+            if (util.isValNaN(val, x.dtype)) {
+                resVals[i] = util.getNaN(res.dtype);
+            }
+            else {
+                resVals[i] = Math.max(0, inVals[i]);
+            }
+        }
+        return res;
+    };
+    MathBackendCPU.prototype.elu = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.dataSync();
+        for (var i = 0; i < values.length; ++i) {
+            var v = values[i];
+            if (v >= 0) {
+                resultValues[i] = v;
+            }
+            else {
+                resultValues[i] = (Math.exp(v) - 1);
+            }
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.eluDer = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.dataSync();
+        for (var i = 0; i < values.length; ++i) {
+            var v = values[i];
+            if (v >= 0) {
+                resultValues[i] = 1;
+            }
+            else {
+                resultValues[i] = Math.exp(v);
+            }
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.selu = function (x) {
+        var scaleAlpha = 1.7580993408473768599402175208123;
+        var scale = 1.0507009873554804934193349852946;
+        var resultValues = new Float32Array(x.size);
+        var values = x.dataSync();
+        for (var i = 0; i < values.length; ++i) {
+            var v = values[i];
+            if (v >= 0) {
+                resultValues[i] = scale * v;
+            }
+            else {
+                resultValues[i] = scaleAlpha * (Math.exp(v) - 1);
+            }
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.leakyRelu = function (x, alpha) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.dataSync();
+        for (var i = 0; i < values.length; i++) {
+            var v = values[i];
+            if (v >= 0) {
+                resultValues[i] = v;
+            }
+            else {
+                resultValues[i] = alpha * v;
+            }
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.prelu = function (x, alpha) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.dataSync();
+        var alphas = alpha.dataSync();
+        for (var i = 0; i < values.length; i++) {
+            var v = values[i];
+            if (v >= 0) {
+                resultValues[i] = v;
+            }
+            else {
+                resultValues[i] = alphas[i] * v;
+            }
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.preluDer = function (x, alpha) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.dataSync();
+        var alphas = alpha.dataSync();
+        for (var i = 0; i < values.length; i++) {
+            var v = values[i];
+            if (v > 0) {
+                resultValues[i] = 1;
+            }
+            else if (v < 0) {
+                resultValues[i] = alphas[i];
+            }
+            else {
+                resultValues[i] = v;
+            }
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.clip = function (x, min, max) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.min(max, Math.max(min, values[i]));
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.abs = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.abs(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.sigmoid = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = 1 / (1 + Math.exp(-values[i]));
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.sin = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.sin(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.cos = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.cos(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.tan = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.tan(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.asin = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.asin(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.acos = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.acos(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.atan = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.atan(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.sinh = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.sinh(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.cosh = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = Math.cosh(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.tanh = function (x) {
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            resultValues[i] = util.tanh(values[i]);
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.step = function (x, alpha) {
+        if (alpha === void 0) { alpha = 0; }
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        for (var i = 0; i < values.length; ++i) {
+            var value = values[i];
+            if (util.isValNaN(value, x.dtype)) {
+                resultValues[i] = util.getNaN(x.dtype);
+            }
+            else {
+                resultValues[i] = value > 0 ? 1 : alpha;
+            }
+        }
+        return ndarray_1.NDArray.make(x.shape, { values: resultValues });
+    };
+    MathBackendCPU.prototype.conv2d = function (x, filter, bias, convInfo) {
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var padLeft = convInfo.padInfo.left;
+        var padTop = convInfo.padInfo.top;
+        var y = ndarray_1.Array4D.zeros(convInfo.outShape);
+        for (var b = 0; b < convInfo.batchSize; ++b) {
+            for (var d2 = 0; d2 < convInfo.outChannels; ++d2) {
+                for (var yR = 0; yR < convInfo.outHeight; ++yR) {
+                    var xRCorner = yR * convInfo.strideHeight - padLeft;
+                    var xRMin = Math.max(0, xRCorner);
+                    var xRMax = Math.min(convInfo.inHeight, filterHeight + xRCorner);
+                    for (var yC = 0; yC < convInfo.outWidth; ++yC) {
+                        var xCCorner = yC * convInfo.strideWidth - padTop;
+                        var xCMin = Math.max(0, xCCorner);
+                        var xCMax = Math.min(convInfo.inWidth, filterWidth + xCCorner);
+                        var dotProd = 0;
+                        for (var xR = xRMin; xR < xRMax; ++xR) {
+                            var wR = xR - xRCorner;
+                            for (var xC = xCMin; xC < xCMax; ++xC) {
+                                var wC = xC - xCCorner;
+                                for (var d1 = 0; d1 < convInfo.inChannels; ++d1) {
+                                    var pixel = x.get(b, xR, xC, d1);
+                                    var weight = filter.get(wR, wC, d1, d2);
+                                    dotProd += pixel * weight;
+                                }
+                            }
+                        }
+                        var biasVal = (bias != null) ? bias.get(d2) : 0;
+                        y.set(dotProd + biasVal, b, yR, yC, d2);
+                    }
+                }
+            }
+        }
+        return y;
+    };
+    MathBackendCPU.prototype.conv2dDerInput = function (dy, filter, convInfo) {
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var topPad = filterHeight - 1 - convInfo.padInfo.top;
+        var leftPad = filterWidth - 1 - convInfo.padInfo.left;
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var dx = ndarray_1.Array4D.zeros(convInfo.inShape);
+        for (var b = 0; b < convInfo.batchSize; ++b) {
+            for (var d1 = 0; d1 < convInfo.inChannels; ++d1) {
+                for (var xR = 0; xR < convInfo.inHeight; ++xR) {
+                    var xRCorner = xR - leftPad;
+                    var xRMin = Math.max(0, Math.ceil(xRCorner / strideHeight));
+                    var yRMax = Math.min(convInfo.outHeight, (filterHeight + xRCorner) / strideHeight);
+                    for (var xC = 0; xC < convInfo.inWidth; ++xC) {
+                        var xCCorner = xC - topPad;
+                        var xCMin = Math.max(0, Math.ceil(xCCorner / strideWidth));
+                        var yCMax = Math.min(convInfo.outWidth, (filterWidth + xCCorner) / strideWidth);
+                        var dotProd = 0;
+                        for (var yR = xRMin; yR < yRMax; ++yR) {
+                            var wR = yR * strideHeight - xRCorner;
+                            for (var yC = xCMin; yC < yCMax; ++yC) {
+                                var wC = yC * strideWidth - xCCorner;
+                                for (var d2 = 0; d2 < convInfo.outChannels; ++d2) {
+                                    var pixel = dy.get(b, yR, yC, d2);
+                                    var weight = filter.get(filterHeight - 1 - wR, filterWidth - 1 - wC, d1, d2);
+                                    dotProd += pixel * weight;
+                                }
+                            }
+                        }
+                        dx.set(dotProd, b, xR, xC, d1);
+                    }
+                }
+            }
+        }
+        return dx;
+    };
+    MathBackendCPU.prototype.conv2dDerFilter = function (x, dy, convInfo) {
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var dW = ndarray_1.Array4D.zeros(convInfo.filterShape);
+        var leftPad = convInfo.padInfo.left;
+        var topPad = convInfo.padInfo.top;
+        for (var wR = 0; wR < filterHeight; ++wR) {
+            var yRMin = Math.max(0, Math.ceil((topPad - wR) / strideHeight));
+            var yRMax = Math.min(convInfo.outHeight, (convInfo.inHeight + topPad - wR) / strideHeight);
+            for (var wC = 0; wC < filterWidth; ++wC) {
+                var yCMin = Math.max(0, Math.ceil((leftPad - wC) / strideWidth));
+                var yCMax = Math.min(convInfo.outWidth, (convInfo.inWidth + leftPad - wC) / strideWidth);
+                for (var d1 = 0; d1 < convInfo.inChannels; ++d1) {
+                    for (var d2 = 0; d2 < convInfo.outChannels; ++d2) {
+                        var dotProd = 0;
+                        for (var b = 0; b < convInfo.batchSize; ++b) {
+                            for (var yR = yRMin; yR < yRMax; ++yR) {
+                                var xR = wR + yR * strideHeight - topPad;
+                                for (var yC = yCMin; yC < yCMax; ++yC) {
+                                    var xC = wC + yC * strideWidth - leftPad;
+                                    dotProd += x.get(b, xR, xC, d1) * dy.get(b, yR, yC, d2);
+                                }
+                            }
+                        }
+                        dW.set(dotProd, wR, wC, d1, d2);
+                    }
+                }
+            }
+        }
+        return dW;
+    };
+    MathBackendCPU.prototype.conv2dDerBias = function (dy) {
+        var _a = dy.shape, batchSize = _a[0], numRows = _a[1], numCols = _a[2], outDepth = _a[3];
+        var values = new Float32Array(outDepth);
+        for (var d2 = 0; d2 < outDepth; ++d2) {
+            var sum = 0;
+            for (var b = 0; b < batchSize; ++b) {
+                for (var r = 0; r < numRows; ++r) {
+                    for (var c = 0; c < numCols; ++c) {
+                        sum += dy.get(b, r, c, d2);
+                    }
+                }
+            }
+            values[d2] = sum;
+        }
+        return ndarray_1.Array1D.new(values);
+    };
+    MathBackendCPU.prototype.depthwiseConv2D = function (x, filter, convInfo) {
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var padLeft = convInfo.padInfo.left;
+        var padTop = convInfo.padInfo.top;
+        var chMul = convInfo.outChannels / convInfo.inChannels;
+        var y = ndarray_1.Array4D.zeros(convInfo.outShape);
+        for (var b = 0; b < convInfo.batchSize; ++b) {
+            for (var d1 = 0; d1 < convInfo.inChannels; ++d1) {
+                for (var yR = 0; yR < convInfo.outHeight; ++yR) {
+                    var xRCorner = yR * convInfo.strideHeight - padLeft;
+                    var xRMin = Math.max(0, xRCorner);
+                    var xRMax = Math.min(convInfo.inHeight, filterHeight + xRCorner);
+                    for (var yC = 0; yC < convInfo.outWidth; ++yC) {
+                        var xCCorner = yC * convInfo.strideWidth - padTop;
+                        var xCMin = Math.max(0, xCCorner);
+                        var xCMax = Math.min(convInfo.inWidth, filterWidth + xCCorner);
+                        for (var q = 0; q < chMul; ++q) {
+                            var dotProd = 0;
+                            for (var xR = xRMin; xR < xRMax; ++xR) {
+                                var wR = xR - xRCorner;
+                                for (var xC = xCMin; xC < xCMax; ++xC) {
+                                    var wC = xC - xCCorner;
+                                    var pixel = x.get(b, xR, xC, d1);
+                                    var weight = filter.get(wR, wC, d1, q);
+                                    dotProd += pixel * weight;
+                                }
+                            }
+                            y.set(dotProd, b, yR, yC, d1 * chMul + q);
+                        }
+                    }
+                }
+            }
+        }
+        return y;
+    };
+    MathBackendCPU.prototype.tile = function (x, reps) {
+        var newShape = new Array(x.rank);
+        for (var i = 0; i < newShape.length; i++) {
+            newShape[i] = x.shape[i] * reps[i];
+        }
+        var dtype;
+        if (x.dtype === 'float32') {
+            dtype = Float32Array;
+        }
+        else if (x.dtype === 'int32') {
+            dtype = Int32Array;
+        }
+        else if (x.dtype === 'bool') {
+            dtype = Uint8Array;
+        }
+        else {
+            throw new Error("Dtype " + x.dtype + " not supported for tile");
+        }
+        var resultValues = new dtype(util.sizeFromShape(newShape));
+        var result = ndarray_1.NDArray.make(newShape, { values: resultValues }, x.dtype);
+        var values = x.getValues();
+        for (var i = 0; i < result.size; ++i) {
+            var newLoc = result.indexToLoc(i);
+            var originalLoc = new Array(x.rank);
+            for (var i_1 = 0; i_1 < originalLoc.length; i_1++) {
+                originalLoc[i_1] = newLoc[i_1] % x.shape[i_1];
+            }
+            var originalIndex = x.locToIndex(originalLoc);
+            resultValues[i] = values[originalIndex];
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.transpose = function (x, perm) {
+        var newShape = new Array(x.rank);
+        for (var i = 0; i < newShape.length; i++) {
+            newShape[i] = x.shape[perm[i]];
+        }
+        var resultValues = new Float32Array(x.size);
+        var values = x.getValues();
+        var result = ndarray_1.NDArray.make(newShape, { values: resultValues });
+        for (var i = 0; i < x.size; ++i) {
+            var loc = x.indexToLoc(i);
+            var newLoc = new Array(loc.length);
+            for (var i_2 = 0; i_2 < newLoc.length; i_2++) {
+                newLoc[i_2] = loc[perm[i_2]];
+            }
+            var newIndex = result.locToIndex(newLoc);
+            resultValues[newIndex] = values[i];
+        }
+        return result;
+    };
+    MathBackendCPU.prototype.pool = function (x, convInfo, poolType) {
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var y = ndarray_1.Array4D.zeros(convInfo.outShape);
+        var padTop = convInfo.padInfo.top;
+        var padLeft = convInfo.padInfo.left;
+        for (var b = 0; b < convInfo.batchSize; ++b) {
+            for (var d = 0; d < convInfo.inChannels; ++d) {
+                for (var yR = 0; yR < convInfo.outHeight; ++yR) {
+                    var xRCorner = yR * strideHeight - padTop;
+                    var xRMin = Math.max(0, xRCorner);
+                    var xRMax = Math.min(convInfo.inHeight, filterHeight + xRCorner);
+                    for (var yC = 0; yC < convInfo.outWidth; ++yC) {
+                        var xCCorner = yC * strideWidth - padLeft;
+                        var xCMin = Math.max(0, xCCorner);
+                        var xCMax = Math.min(convInfo.inWidth, filterWidth + xCCorner);
+                        var minMaxValue = (poolType === 'max' ? Number.NEGATIVE_INFINITY :
+                            Number.POSITIVE_INFINITY);
+                        var avgValue = 0;
+                        for (var xR = xRMin; xR < xRMax; ++xR) {
+                            for (var xC = xCMin; xC < xCMax; ++xC) {
+                                var pixel = x.get(b, xR, xC, d);
+                                if (isNaN(pixel)) {
+                                    minMaxValue = NaN;
+                                    avgValue = NaN;
+                                    break;
+                                }
+                                if ((poolType === 'max' && pixel > minMaxValue) ||
+                                    (poolType === 'min' && pixel < minMaxValue)) {
+                                    minMaxValue = pixel;
+                                }
+                                else if (poolType === 'avg') {
+                                    avgValue += pixel / (filterHeight * filterWidth);
+                                }
+                            }
+                            if (isNaN(minMaxValue)) {
+                                break;
+                            }
+                        }
+                        y.set(poolType === 'avg' ? avgValue : minMaxValue, b, yR, yC, d);
+                    }
+                }
+            }
+        }
+        return y;
+    };
+    MathBackendCPU.prototype.maxPool = function (x, convInfo) {
+        return this.pool(x, convInfo, 'max');
+    };
+    MathBackendCPU.prototype.maxPoolPositions = function (x, convInfo) {
+        var maxPositions = ndarray_1.Array4D.zeros(convInfo.outShape);
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var padTop = convInfo.padInfo.top;
+        var padLeft = convInfo.padInfo.left;
+        for (var b = 0; b < convInfo.batchSize; ++b) {
+            for (var d = 0; d < convInfo.inChannels; ++d) {
+                for (var yR = 0; yR < convInfo.outHeight; ++yR) {
+                    var xRCorner = yR * strideHeight - padTop;
+                    var xRMin = Math.max(0, xRCorner);
+                    var xRMax = Math.min(convInfo.inHeight, filterHeight + xRCorner);
+                    for (var yC = 0; yC < convInfo.outWidth; ++yC) {
+                        var xCCorner = yC * strideWidth - padLeft;
+                        var xCMin = Math.max(0, xCCorner);
+                        var xCMax = Math.min(convInfo.inWidth, filterWidth + xCCorner);
+                        var maxValue = Number.NEGATIVE_INFINITY;
+                        var maxPosition = -1;
+                        for (var xR = xRMin; xR < xRMax; ++xR) {
+                            var wR = xR - xRCorner;
+                            for (var xC = xCMin; xC < xCMax; ++xC) {
+                                var wC = xC - xCCorner;
+                                var pixel = x.get(b, xR, xC, d);
+                                if (pixel > maxValue) {
+                                    maxValue = pixel;
+                                    maxPosition = wR * filterWidth + wC;
+                                }
+                            }
+                        }
+                        maxPositions.set(maxPosition, b, yR, yC, d);
+                    }
+                }
+            }
+        }
+        return maxPositions;
+    };
+    MathBackendCPU.prototype.maxPoolBackprop = function (dy, x, convInfo) {
+        var maxPositions = this.maxPoolPositions(x, convInfo);
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var padLeft = filterWidth - 1 - convInfo.padInfo.left;
+        var padTop = filterHeight - 1 - convInfo.padInfo.top;
+        var dx = ndarray_1.Array4D.zeros(x.shape);
+        for (var b = 0; b < convInfo.batchSize; ++b) {
+            for (var d = 0; d < convInfo.inChannels; ++d) {
+                for (var dxR = 0; dxR < convInfo.inHeight; ++dxR) {
+                    for (var dxC = 0; dxC < convInfo.inWidth; ++dxC) {
+                        var dyRCorner = dxR - padTop;
+                        var dyCCorner = dxC - padLeft;
+                        var dotProd = 0;
+                        for (var wR = 0; wR < filterHeight; ++wR) {
+                            var dyR = (dyRCorner + wR) / strideHeight;
+                            if (dyR < 0 || dyR >= convInfo.outHeight ||
+                                Math.floor(dyR) !== dyR) {
+                                continue;
+                            }
+                            for (var wC = 0; wC < filterWidth; ++wC) {
+                                var dyC = (dyCCorner + wC) / strideWidth;
+                                if (dyC < 0 || dyC >= convInfo.outWidth ||
+                                    Math.floor(dyC) !== dyC) {
+                                    continue;
+                                }
+                                var maxPos = filterHeight * filterWidth - 1 -
+                                    maxPositions.get(b, dyR, dyC, d);
+                                var curPos = wR * filterWidth + wC;
+                                var mask = maxPos === curPos ? 1 : 0;
+                                if (mask === 0) {
+                                    continue;
+                                }
+                                var pixel = dy.get(b, dyR, dyC, d);
+                                dotProd += pixel * mask;
+                            }
+                        }
+                        dx.set(dotProd, b, dxR, dxC, d);
+                    }
+                }
+            }
+        }
+        return dx;
+    };
+    MathBackendCPU.prototype.minPool = function (x, convInfo) {
+        return this.pool(x, convInfo, 'min');
+    };
+    MathBackendCPU.prototype.avgPool = function (x, convInfo) {
+        return this.pool(x, convInfo, 'avg');
+    };
+    MathBackendCPU.prototype.resizeBilinear3D = function (x, newShape2D, alignCorners) {
+        var output = ndarray_1.Array3D.zeros([newShape2D[0], newShape2D[1], x.shape[2]]);
+        var effectiveInputSize = alignCorners ? [x.shape[0] - 1, x.shape[1] - 1, x.shape[2]] : x.shape;
+        var effectiveOutputSize = alignCorners ?
+            [output.shape[0] - 1, output.shape[1] - 1, output.shape[2]] :
+            output.shape;
+        for (var r = 0; r < output.shape[0]; r++) {
+            for (var c = 0; c < output.shape[1]; c++) {
+                for (var d = 0; d < output.shape[2]; d++) {
+                    var sourceFracRow = (effectiveInputSize[0]) * r / (effectiveOutputSize[0]);
+                    var sourceFracCol = (effectiveInputSize[1]) * c / (effectiveOutputSize[1]);
+                    var sourceRowFloor = Math.floor(sourceFracRow);
+                    var sourceRowCeil = Math.min(x.shape[0] - 1, Math.ceil(sourceFracRow));
+                    var sourceColFloor = Math.floor(sourceFracCol);
+                    var sourceColCeil = Math.min(x.shape[1] - 1, Math.ceil(sourceFracCol));
+                    var topLeft = x.get(sourceRowFloor, sourceColFloor, d);
+                    var bottomLeft = x.get(sourceRowCeil, sourceColFloor, d);
+                    var topRight = x.get(sourceRowFloor, sourceColCeil, d);
+                    var bottomRight = x.get(sourceRowCeil, sourceColCeil, d);
+                    var rowFrac = sourceFracRow - sourceRowFloor;
+                    var colFrac = sourceFracCol - sourceColFloor;
+                    var top_1 = topLeft + (topRight - topLeft) * colFrac;
+                    var bottom = bottomLeft + (bottomRight - bottomLeft) * colFrac;
+                    var newValue = top_1 + (bottom - top_1) * rowFrac;
+                    output.set(newValue, r, c, d);
+                }
+            }
+        }
+        return output;
+    };
+    MathBackendCPU.prototype.batchNormalization2D = function (x, mean, variance, varianceEpsilon, scale, offset) {
+        var xValues = x.getValues();
+        var meanValues = mean.getValues();
+        var varianceValues = variance.getValues();
+        var scaleValues = scale ? scale.getValues() : new Float32Array([1]);
+        var offsetValues = offset ? offset.getValues() : new Float32Array([0]);
+        var outValues = new Float32Array(xValues.length);
+        for (var i = 0; i < xValues.length; i++) {
+            outValues[i] = offsetValues[i % offsetValues.length] +
+                (xValues[i] - meanValues[i % meanValues.length]) *
+                    scaleValues[i % scaleValues.length] /
+                    Math.sqrt(varianceValues[i % varianceValues.length] + varianceEpsilon);
+        }
+        return ndarray_1.Array2D.new(x.shape, outValues);
+    };
+    MathBackendCPU.prototype.batchNormalization3D = function (x, mean, variance, varianceEpsilon, scale, offset) {
+        var xValues = x.getValues();
+        var meanValues = mean.getValues();
+        var varianceValues = variance.getValues();
+        var scaleValues = scale ? scale.getValues() : new Float32Array([1]);
+        var offsetValues = offset ? offset.getValues() : new Float32Array([0]);
+        var outValues = new Float32Array(xValues.length);
+        for (var i = 0; i < xValues.length; i++) {
+            outValues[i] = offsetValues[i % offsetValues.length] +
+                (xValues[i] - meanValues[i % meanValues.length]) *
+                    scaleValues[i % scaleValues.length] /
+                    Math.sqrt(varianceValues[i % varianceValues.length] + varianceEpsilon);
+        }
+        return ndarray_1.Array3D.new(x.shape, outValues);
+    };
+    MathBackendCPU.prototype.multinomial = function (probabilities, numSamples, seed) {
+        var batchSize = probabilities.shape[0];
+        var numEvents = probabilities.shape[1];
+        var res = ndarray_1.Array2D.zeros([batchSize, numSamples], 'int32');
+        var resVals = res.getValues();
+        var probVals = probabilities.getValues();
+        for (var b = 0; b < batchSize; ++b) {
+            var offset = b * numEvents;
+            var cdf = new Float32Array(numEvents - 1);
+            cdf[0] = probVals[offset];
+            for (var event_1 = 1; event_1 < cdf.length; ++event_1) {
+                cdf[event_1] = cdf[event_1 - 1] + probVals[offset + event_1];
+            }
+            var random = seedrandom.alea(seed.toString());
+            var outOffset = b * numSamples;
+            for (var sampleId = 0; sampleId < numSamples; ++sampleId) {
+                var r = random();
+                resVals[outOffset + sampleId] = cdf.length;
+                for (var event_2 = 0; event_2 < cdf.length; event_2++) {
+                    if (r < cdf[event_2]) {
+                        resVals[outOffset + sampleId] = event_2;
+                        break;
+                    }
+                }
+            }
+        }
+        return res;
+    };
+    MathBackendCPU.prototype.oneHot = function (indices, depth, onValue, offValue) {
+        var res = new Float32Array(indices.size * depth);
+        res.fill(offValue);
+        for (var event_3 = 0; event_3 < indices.size; ++event_3) {
+            res[event_3 * depth + indices.get(event_3)] = onValue;
+        }
+        return ndarray_1.Array2D.new([indices.size, depth], res);
+    };
+    MathBackendCPU.prototype.broadcastedBinaryOp = function (a, b, dtype, op) {
+        var newShape = broadcast_util.assertAndGetBroadcastShape(a.shape, b.shape);
+        var result = ndarray_1.NDArray.zeros(newShape, dtype);
+        var newValues = result.getValues();
+        var aValues = a.getValues();
+        var bValues = b.getValues();
+        var aBroadcastDims = broadcast_util.getBroadcastDims(a.shape, newShape);
+        var bBroadcastDims = broadcast_util.getBroadcastDims(b.shape, newShape);
+        var _loop_1 = function (i) {
+            var loc = result.indexToLoc(i);
+            var aLoc = loc.slice(-a.rank);
+            aBroadcastDims.forEach(function (d) { return aLoc[d] = 0; });
+            var aIndex = a.locToIndex(aLoc);
+            var bLoc = loc.slice(-b.rank);
+            bBroadcastDims.forEach(function (d) { return bLoc[d] = 0; });
+            var bIndex = b.locToIndex(bLoc);
+            newValues[i] = op(aValues[aIndex], bValues[bIndex]);
+        };
+        for (var i = 0; i < newValues.length; ++i) {
+            _loop_1(i);
+        }
+        return result;
+    };
+    return MathBackendCPU;
+}());
+exports.MathBackendCPU = MathBackendCPU;
+environment_1.ENV.registerBackend('cpu', function () { return new MathBackendCPU(); });
+var NDArrayMathCPU = (function (_super) {
+    __extends(NDArrayMathCPU, _super);
+    function NDArrayMathCPU(safeMode) {
+        if (safeMode === void 0) { safeMode = false; }
+        var _this = this;
+        console.warn('new NDArrayMathCPU() is deprecated. Please use the global ' +
+            'dl.ENV.math. In rare cases, to construct your own NDArrayMath ' +
+            'that runs on CPU, use math = new NDArrayMath(\'cpu\', safeMode); ' +
+            'and make sure to set it as global: dl.ENV.setMath(math);');
+        _this = _super.call(this, 'cpu', safeMode) || this;
+        environment_1.ENV.setMath(_this);
+        return _this;
+    }
+    return NDArrayMathCPU;
+}(math_1.NDArrayMath));
+exports.NDArrayMathCPU = NDArrayMathCPU;
+
+},{"../../environment":15,"../../util":101,"../broadcast_util":90,"../concat_util":91,"../math":94,"../ndarray":95,"../types":99,"./../axis_util":54,"./types/matmul":61,"seedrandom":2}],56:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../../util");
+var kernel_registry = require("./kernel_registry");
+var tape_1 = require("./tape");
+var BackendEngine = (function () {
+    function BackendEngine(backend) {
+        this.backend = backend;
+        this.debugMode = false;
+        this.masterTape = new tape_1.Tape(backend);
+    }
+    BackendEngine.prototype.enableDebugMode = function () {
+        this.debugMode = true;
+    };
+    BackendEngine.prototype.executeKernel = function (kernelName, config, grad) {
+        var _this = this;
+        var kernelFn = function () {
+            return kernel_registry.executeKernel(_this.backend, kernelName, config);
+        };
+        var start;
+        if (this.debugMode) {
+            start = performance.now();
+        }
+        var result = kernelFn();
+        if (this.debugMode) {
+            var vals = result.getValues();
+            var time = util.rightPad(performance.now() - start + "ms", 9);
+            var paddedName = util.rightPad(name, 25);
+            var rank = result.rank;
+            var size = result.size;
+            var shape = util.rightPad(result.shape.toString(), 14);
+            console.log("%c" + paddedName + "\t%c" + time + "\t%c" + rank + "D " + shape + "\t%c" + size, 'font-weight:bold', 'color:red', 'color:blue', 'color: orange');
+            this.checkForNaN(vals, result.dtype, name);
+        }
+        var evaluatedNode = {
+            name: "kernel: " + kernelName,
+            kernel: kernelName,
+            inputAndArgs: config,
+            output: result,
+            gradient: grad
+        };
+        this.masterTape.addEvaluatedKernelNode(evaluatedNode);
+        return result;
+    };
+    BackendEngine.prototype.gradientWrt = function (y, xs) {
+        return this.masterTape.gradientWrt(y, xs);
+    };
+    BackendEngine.prototype.checkForNaN = function (vals, dtype, name) {
+        for (var i = 0; i < vals.length; i++) {
+            if (util.isValNaN(vals[i], dtype)) {
+                throw Error("The result of the last math." + name + " has NaNs.");
+            }
+        }
+    };
+    BackendEngine.prototype.getBackend = function () {
+        return this.backend;
+    };
+    return BackendEngine;
+}());
+exports.BackendEngine = BackendEngine;
+
+},{"../../util":101,"./kernel_registry":58,"./tape":59}],57:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __generator = (this && this.__generator) || function (thisArg, body) {
+    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
+    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
+    function verb(n) { return function (v) { return step([n, v]); }; }
+    function step(op) {
+        if (f) throw new TypeError("Generator is already executing.");
+        while (_) try {
+            if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
+            if (y = 0, t) op = [0, t.value];
+            switch (op[0]) {
+                case 0: case 1: t = op; break;
+                case 4: _.label++; return { value: op[1], done: false };
+                case 5: _.label++; y = op[1]; op = [0]; continue;
+                case 7: op = _.ops.pop(); _.trys.pop(); continue;
+                default:
+                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
+                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
+                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
+                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
+                    if (t[2]) _.ops.pop();
+                    _.trys.pop(); continue;
+            }
+            op = body.call(thisArg, _);
+        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
+        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
+    }
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../environment");
+var util = require("../../util");
+var axis_util = require("../axis_util");
+var math_1 = require("../math");
+var ndarray_1 = require("../ndarray");
+var reduce_util = require("../reduce_util");
+var types_1 = require("../types");
+var argminmax_gpu_1 = require("./webgl/argminmax_gpu");
+var batchnorm_gpu_1 = require("./webgl/batchnorm_gpu");
+var binaryop_gpu = require("./webgl/binaryop_gpu");
+var binaryop_gpu_1 = require("./webgl/binaryop_gpu");
+var clip_gpu_1 = require("./webgl/clip_gpu");
+var concat_gpu_1 = require("./webgl/concat_gpu");
+var conv_backprop_gpu_1 = require("./webgl/conv_backprop_gpu");
+var conv_gpu_1 = require("./webgl/conv_gpu");
+var conv_gpu_depthwise_1 = require("./webgl/conv_gpu_depthwise");
+var copy_gpu_1 = require("./webgl/copy_gpu");
+var gpgpu_context_1 = require("./webgl/gpgpu_context");
+var gpgpu_math = require("./webgl/gpgpu_math");
+var gpgpu_util = require("./webgl/gpgpu_util");
+var max_pool_backprop_gpu_1 = require("./webgl/max_pool_backprop_gpu");
+var mulmat_gpu_1 = require("./webgl/mulmat_gpu");
+var multinomial_gpu_1 = require("./webgl/multinomial_gpu");
+var onehot_gpu_1 = require("./webgl/onehot_gpu");
+var pool_gpu_1 = require("./webgl/pool_gpu");
+var reduce_gpu_1 = require("./webgl/reduce_gpu");
+var resize_bilinear_gpu_1 = require("./webgl/resize_bilinear_gpu");
+var slice_gpu_1 = require("./webgl/slice_gpu");
+var tex_util_1 = require("./webgl/tex_util");
+var texture_manager_1 = require("./webgl/texture_manager");
+var tile_gpu_1 = require("./webgl/tile_gpu");
+var transpose_gpu_1 = require("./webgl/transpose_gpu");
+var unary_op = require("./webgl/unaryop_gpu");
+var unaryop_gpu_1 = require("./webgl/unaryop_gpu");
+var webgl_util = require("./webgl/webgl_util");
+var MathBackendWebGL = (function () {
+    function MathBackendWebGL(gpgpu) {
+        this.texData = {};
+        this.binaryCache = {};
+        if (environment_1.ENV.get('WEBGL_VERSION') < 1) {
+            throw new Error('WebGL is not supported on this device');
+        }
+        if (gpgpu == null) {
+            var gl = gpgpu_util.createWebGLContext();
+            this.gpgpu = new gpgpu_context_1.GPGPUContext(gl);
+            this.gpgpuCreatedLocally = true;
+        }
+        else {
+            this.gpgpu = gpgpu;
+            this.gpgpuCreatedLocally = false;
+        }
+        this.textureManager = new texture_manager_1.TextureManager(this.gpgpu);
+    }
+    MathBackendWebGL.prototype.writePixels = function (id, pixels, numChannels) {
+        var shape = [pixels.height, pixels.width, numChannels];
+        var texShape = [shape[0], shape[1]];
+        var texture = this.textureManager.acquireTexture(texShape);
+        this.gpgpu.uploadPixelDataToTexture(texture, pixels);
+        this.texData[id] = {
+            texture: texture,
+            textureType: tex_util_1.TextureType.RGBA_COLOR,
+            texShape: texShape,
+            numChannels: numChannels,
+            dtype: 'int32'
+        };
+    };
+    MathBackendWebGL.prototype.write = function (id, values, dtype, shape) {
+        var texShape = webgl_util.getTextureShapeFromLogicalShape(this.gpgpu.gl, shape);
+        var texture = this.textureManager.acquireTexture(texShape);
+        var textureType = tex_util_1.TextureType.DEFAULT;
+        this.texData[id] = { texture: texture, textureType: textureType, texShape: texShape, dtype: dtype };
+        if (values != null) {
+            this.gpgpu.uploadMatrixToTexture(texture, texShape[0], texShape[1], typedArrayToFloat32(values, dtype));
+        }
+    };
+    MathBackendWebGL.prototype.readSync = function (id) {
+        this.throwIfNoData(id);
+        var values;
+        var _a = this.texData[id], texture = _a.texture, textureType = _a.textureType, texShape = _a.texShape, numChannels = _a.numChannels, dtype = _a.dtype;
+        if (textureType === tex_util_1.TextureType.DEFAULT) {
+            values = this.gpgpu.downloadMatrixFromTexture(texture, texShape[0], texShape[1]);
+        }
+        else {
+            values = this.gpgpu.downloadMatrixFromRGBAColorTexture(texture, texShape[0], texShape[1], numChannels);
+        }
+        return float32ToTypedArray(values, dtype);
+    };
+    MathBackendWebGL.prototype.read = function (id) {
+        return __awaiter(this, void 0, void 0, function () {
+            var _a, texture, textureType, texShape;
+            return __generator(this, function (_b) {
+                switch (_b.label) {
+                    case 0:
+                        this.throwIfNoData(id);
+                        _a = this.texData[id], texture = _a.texture, textureType = _a.textureType, texShape = _a.texShape;
+                        if (environment_1.ENV.get('WEBGL_GET_BUFFER_SUB_DATA_ASYNC_EXTENSION_ENABLED') &&
+                            textureType === tex_util_1.TextureType.DEFAULT) {
+                            return [2, this.gpgpu.downloadMatrixFromTextureAsync(texture, texShape[0], texShape[1])];
+                        }
+                        if (!environment_1.ENV.get('WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_ENABLED')) {
+                            return [2, this.readSync(id)];
+                        }
+                        return [4, this.gpgpu.runQuery(function () { })];
+                    case 1:
+                        _b.sent();
+                        return [2, this.readSync(id)];
+                }
+            });
+        });
+    };
+    MathBackendWebGL.prototype.time = function (query) {
+        return __awaiter(this, void 0, void 0, function () {
+            var start, a;
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0:
+                        if (!!environment_1.ENV.get('WEBGL_DISJOINT_QUERY_TIMER_EXTENSION_ENABLED')) return [3, 2];
+                        start = performance.now();
+                        a = query();
+                        return [4, a.data()];
+                    case 1:
+                        _a.sent();
+                        return [2, performance.now() - start];
+                    case 2: return [2, this.gpgpu.runQuery(query)];
+                }
+            });
+        });
+    };
+    MathBackendWebGL.prototype.disposeData = function (id) {
+        if (id in this.texData) {
+            var _a = this.texData[id], texture = _a.texture, texShape = _a.texShape;
+            this.textureManager.releaseTexture(texture, texShape);
+            delete this.texData[id];
+        }
+    };
+    MathBackendWebGL.prototype.getTexture = function (id) {
+        this.throwIfNoData(id);
+        return this.texData[id].texture;
+    };
+    MathBackendWebGL.prototype.getTextureData = function (id) {
+        this.throwIfNoData(id);
+        return this.texData[id];
+    };
+    MathBackendWebGL.prototype.getGPGPUContext = function () {
+        return this.gpgpu;
+    };
+    MathBackendWebGL.prototype.clone = function (x) {
+        this.throwIfNoData(x.id);
+        var texShape = this.texData[x.id].texShape;
+        var source = x.as2D(texShape[0], texShape[1]);
+        var output = this.makeOutputArray(texShape, x.dtype);
+        this.copy2D(source, [0, 0], texShape, output, [0, 0], texShape);
+        return output.reshape(x.shape);
+    };
+    MathBackendWebGL.prototype.slice1D = function (x, begin, size) {
+        var program = new slice_gpu_1.SliceProgram([size]);
+        var customSetup = program.getCustomSetupFunc([begin]);
+        return this.compileAndRun(program, [x], null, customSetup);
+    };
+    MathBackendWebGL.prototype.slice2D = function (x, begin, size) {
+        var program = new slice_gpu_1.SliceProgram(size);
+        var customSetup = program.getCustomSetupFunc(begin);
+        return this.compileAndRun(program, [x], null, customSetup);
+    };
+    MathBackendWebGL.prototype.slice3D = function (x, begin, size) {
+        var program = new slice_gpu_1.SliceProgram(size);
+        var customSetup = program.getCustomSetupFunc(begin);
+        return this.compileAndRun(program, [x], null, customSetup);
+    };
+    MathBackendWebGL.prototype.slice4D = function (x, begin, size) {
+        var program = new slice_gpu_1.SliceProgram(size);
+        var customSetup = program.getCustomSetupFunc(begin);
+        return this.compileAndRun(program, [x], null, customSetup);
+    };
+    MathBackendWebGL.prototype.copy2D = function (source, sourceBeginRowCol, sourceSizeRowCol, dest, destBeginRowCol, destSizeRowCol) {
+        var program = new copy_gpu_1.Copy2DProgram(sourceSizeRowCol[1], destSizeRowCol[1]);
+        var customSetup = program.getCustomSetupFunc(sourceBeginRowCol, destBeginRowCol, destSizeRowCol);
+        this.compileAndRun(program, [source], dest, customSetup);
+    };
+    MathBackendWebGL.prototype.concat1D = function (a, b) {
+        var program = new concat_gpu_1.ConcatProgram(a.shape, b.shape, 0);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.concat2D = function (a, b, axis) {
+        var program = new concat_gpu_1.ConcatProgram(a.shape, b.shape, axis);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.concat3D = function (a, b, axis) {
+        var program = new concat_gpu_1.ConcatProgram(a.shape, b.shape, axis);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.concat4D = function (a, b, axis) {
+        var program = new concat_gpu_1.ConcatProgram(a.shape, b.shape, axis);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.neg = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.NEG);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.matMul = function (a, b, aOrientation, bOrientation) {
+        var program = new mulmat_gpu_1.MatMulProgram(a.shape, b.shape, aOrientation, bOrientation);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.multiply = function (a, b) {
+        var program = new binaryop_gpu_1.BinaryOpProgram(binaryop_gpu.MUL, a.shape, b.shape);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.batchNormalization2D = function (x, mean, variance, varianceEpsilon, scale, offset) {
+        var inputs = [x, mean, variance];
+        var offsetShape = null;
+        if (offset != null) {
+            offsetShape = offset.shape;
+            inputs.push(offset);
+        }
+        var scaleShape = null;
+        if (scale != null) {
+            scaleShape = scale.shape;
+            inputs.push(scale);
+        }
+        var program = new batchnorm_gpu_1.BatchNormProgram(x.shape, mean.shape, variance.shape, offsetShape, scaleShape, varianceEpsilon);
+        return this.compileAndRun(program, inputs);
+    };
+    MathBackendWebGL.prototype.batchNormalization3D = function (x, mean, variance, varianceEpsilon, scale, offset) {
+        var inputs = [x, mean, variance];
+        var offsetShape = null;
+        if (offset != null) {
+            offsetShape = offset.shape;
+            inputs.push(offset);
+        }
+        var scaleShape = null;
+        if (scale != null) {
+            scaleShape = scale.shape;
+            inputs.push(scale);
+        }
+        var program = new batchnorm_gpu_1.BatchNormProgram(x.shape, mean.shape, variance.shape, offsetShape, scaleShape, varianceEpsilon);
+        return this.compileAndRun(program, inputs);
+    };
+    MathBackendWebGL.prototype.tile = function (x, reps) {
+        var program = new tile_gpu_1.TileProgram(x.shape, reps);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.transpose = function (x, perm) {
+        var program = new transpose_gpu_1.TransposeProgram(x.shape, perm);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.reduce = function (x, reduceType, dtype) {
+        var batchSize = x.shape[0];
+        var inSize = x.shape[1];
+        var windowSize = reduce_util.computeOptimalWindowSize(inSize);
+        var reduceInfo = { windowSize: windowSize, inSize: inSize, batchSize: batchSize };
+        var program = new reduce_gpu_1.ReduceProgram(reduceInfo, reduceType);
+        var _a = program.outputShape, rows = _a[0], cols = _a[1];
+        var output = this.makeOutputArray(program.outputShape, dtype).as2D(rows, cols);
+        this.compileAndRun(program, [x], output);
+        if (output.shape[1] === 1) {
+            return output;
+        }
+        return this.reduce(output, reduceType, dtype);
+    };
+    MathBackendWebGL.prototype.argReduce = function (x, reduceType, bestIndicesA) {
+        if (bestIndicesA === void 0) { bestIndicesA = null; }
+        var batchSize = x.shape[0];
+        var inSize = x.shape[1];
+        if (bestIndicesA != null) {
+            batchSize = bestIndicesA.shape[0];
+            inSize = bestIndicesA.shape[1];
+        }
+        var windowSize = reduce_util.computeOptimalWindowSize(inSize);
+        var reduceInfo = { windowSize: windowSize, inSize: inSize, batchSize: batchSize };
+        var program = new argminmax_gpu_1.ArgMinMaxProgram(reduceInfo, reduceType, bestIndicesA == null);
+        var _a = program.outputShape, rows = _a[0], cols = _a[1];
+        var output = this.makeOutputArray(program.outputShape, 'int32').as2D(rows, cols);
+        var inputs = [x];
+        if (bestIndicesA != null) {
+            inputs.push(bestIndicesA);
+        }
+        this.compileAndRun(program, inputs, output);
+        if (output.shape[1] === 1) {
+            return output;
+        }
+        return this.argReduce(x, reduceType, output);
+    };
+    MathBackendWebGL.prototype.sum = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('sum', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var inSize = util.sizeFromShape(reduceShape);
+        var a2D = x.as2D(-1, inSize);
+        var outputDType = types_1.SumTypesMap[x.dtype];
+        return this.reduce(a2D, 'sum', outputDType).reshape(outShape);
+    };
+    MathBackendWebGL.prototype.argMin = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('argMin', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var inSize = util.sizeFromShape(reduceShape);
+        var a2D = x.as2D(-1, inSize);
+        return this.argReduce(a2D, 'min').reshape(outShape);
+    };
+    MathBackendWebGL.prototype.argMax = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('argMax', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var inSize = util.sizeFromShape(reduceShape);
+        var a2D = x.as2D(-1, inSize);
+        return this.argReduce(a2D, 'max').reshape(outShape);
+    };
+    MathBackendWebGL.prototype.equal = function (a, b) {
+        var program = new binaryop_gpu_1.BinaryOpProgram(binaryop_gpu.EQUAL, a.shape, b.shape);
+        var output = this.makeOutputArray(program.outputShape, 'bool');
+        return this.compileAndRun(program, [a, b], output);
+    };
+    MathBackendWebGL.prototype.topKValues = function (x, k) {
+        throw new Error('topKValues GPU not yet implemented!');
+    };
+    MathBackendWebGL.prototype.topKIndices = function (x, k) {
+        throw new Error('topKIndices GPU not yet implemented!');
+    };
+    MathBackendWebGL.prototype.min = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('min', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var inSize = util.sizeFromShape(reduceShape);
+        var a2D = x.as2D(-1, inSize);
+        return this.reduce(a2D, 'min', a2D.dtype).reshape(outShape);
+    };
+    MathBackendWebGL.prototype.max = function (x, axes) {
+        axis_util.assertAxesAreInnerMostDims('max', axes, x.rank);
+        var _a = axis_util.computeOutAndReduceShapes(x.shape, axes), outShape = _a[0], reduceShape = _a[1];
+        var inSize = util.sizeFromShape(reduceShape);
+        var a2D = x.as2D(-1, inSize);
+        return this.reduce(a2D, 'max', a2D.dtype).reshape(outShape);
+    };
+    MathBackendWebGL.prototype.divide = function (a, b) {
+        var program = new binaryop_gpu_1.BinaryOpProgram(binaryop_gpu.DIV, a.shape, b.shape);
+        var output = this.makeOutputArray(program.outputShape, 'float32');
+        return this.compileAndRun(program, [a, b], output);
+    };
+    MathBackendWebGL.prototype.add = function (a, b) {
+        var program = new binaryop_gpu_1.BinaryOpProgram(binaryop_gpu.ADD, a.shape, b.shape);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.subtract = function (a, b) {
+        var program = new binaryop_gpu_1.BinaryOpProgram(binaryop_gpu.SUB, a.shape, b.shape);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.pow = function (a, b) {
+        var program = new binaryop_gpu_1.BinaryOpProgram(binaryop_gpu.POW, a.shape, b.shape);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.ceil = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.CEIL);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.floor = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.FLOOR);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.exp = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.EXP);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.log = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.LOG);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.sqrt = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.SQRT);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.square = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.SQUARE);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.relu = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.RELU);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.elu = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.ELU);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.eluDer = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.ELU_DER);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.selu = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.SELU);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.leakyRelu = function (x, alpha) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.LEAKY_RELU(alpha));
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.prelu = function (a, b) {
+        var program = new binaryop_gpu_1.BinaryOpProgram(binaryop_gpu.PRELU, a.shape, b.shape);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.preluDer = function (a, b) {
+        var program = new binaryop_gpu_1.BinaryOpProgram(binaryop_gpu.PRELU_DER, a.shape, b.shape);
+        return this.compileAndRun(program, [a, b]);
+    };
+    MathBackendWebGL.prototype.clip = function (x, min, max) {
+        var program = new clip_gpu_1.ClipProgram(x.shape, min, max);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.abs = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.ABS);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.sigmoid = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.SIGMOID);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.sin = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.SIN);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.cos = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.COS);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.tan = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.TAN);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.asin = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.ASIN);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.acos = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.ACOS);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.atan = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.ATAN);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.sinh = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.SINH);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.cosh = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.COSH);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.tanh = function (x) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.TANH);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.step = function (x, alpha) {
+        var program = new unaryop_gpu_1.UnaryOpProgram(x.shape, unary_op.STEP(alpha));
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.conv2d = function (x, filter, bias, convInfo) {
+        var program = new conv_gpu_1.Conv2DProgram(convInfo, bias != null);
+        var inputs = bias != null ? [x, filter, bias] : [x, filter];
+        return this.compileAndRun(program, inputs);
+    };
+    MathBackendWebGL.prototype.conv2dDerInput = function (dy, filter, convInfo) {
+        var program = new conv_backprop_gpu_1.Conv2DDerInputProgram(convInfo);
+        return this.compileAndRun(program, [dy, filter]);
+    };
+    MathBackendWebGL.prototype.conv2dDerFilter = function (x, dy, convInfo) {
+        var program = new conv_backprop_gpu_1.Conv2DDerFilterProgram(convInfo);
+        return this.compileAndRun(program, [x, dy]);
+    };
+    MathBackendWebGL.prototype.conv2dDerBias = function (dy) {
+        var program = new conv_backprop_gpu_1.Conv2DDerBiasProgram(dy.shape);
+        return this.compileAndRun(program, [dy]);
+    };
+    MathBackendWebGL.prototype.depthwiseConv2D = function (x, filter, convInfo) {
+        var program = new conv_gpu_depthwise_1.DepthwiseConv2DProgram(convInfo);
+        return this.compileAndRun(program, [x, filter]);
+    };
+    MathBackendWebGL.prototype.maxPool = function (x, convInfo) {
+        var program = new pool_gpu_1.Pool2DProgram(convInfo, 'max', false);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.minPool = function (x, convInfo) {
+        var program = new pool_gpu_1.Pool2DProgram(convInfo, 'min', false);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.avgPool = function (x, convInfo) {
+        var program = new pool_gpu_1.Pool2DProgram(convInfo, 'avg', false);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.maxPoolBackprop = function (dy, x, convInfo) {
+        var getPositions = true;
+        var maxPoolPositionsProgram = new pool_gpu_1.Pool2DProgram(convInfo, 'max', getPositions);
+        var maxPoolPositions = this.compileAndRun(maxPoolPositionsProgram, [x]);
+        var maxPoolBackPropProgram = new max_pool_backprop_gpu_1.MaxPool2DBackpropProgram(convInfo);
+        var result = this.compileAndRun(maxPoolBackPropProgram, [dy, maxPoolPositions]);
+        maxPoolPositions.dispose();
+        return result;
+    };
+    MathBackendWebGL.prototype.resizeBilinear3D = function (x, newShape2D, alignCorners) {
+        var program = new resize_bilinear_gpu_1.ResizeBilinear3DProgram(x.shape, newShape2D, alignCorners);
+        return this.compileAndRun(program, [x]);
+    };
+    MathBackendWebGL.prototype.multinomial = function (probs, numSamples, seed) {
+        var batchSize = probs.shape[0];
+        var numOutcomes = probs.shape[1];
+        var program = new multinomial_gpu_1.MultinomialProgram(batchSize, numOutcomes, numSamples);
+        var output = this.makeOutputArray(program.outputShape, 'int32');
+        var customSetup = program.getCustomSetupFunc(seed);
+        return this.compileAndRun(program, [probs], output, customSetup);
+    };
+    MathBackendWebGL.prototype.oneHot = function (indices, depth, onValue, offValue) {
+        var program = new onehot_gpu_1.OneHotProgram(indices.size, depth, onValue, offValue);
+        return this.compileAndRun(program, [indices]);
+    };
+    MathBackendWebGL.prototype.makeOutputArray = function (shape, dtype) {
+        return ndarray_1.NDArray.make(shape, {}, dtype);
+    };
+    MathBackendWebGL.prototype.compileAndRun = function (program, inputs, output, customSetup) {
+        var _this = this;
+        if (output == null) {
+            output = this.makeOutputArray(program.outputShape, inputs[0].dtype);
+        }
+        var inputsData = inputs.map(function (input) {
+            _this.throwIfNoData(input.id);
+            return { array: input, texData: _this.texData[input.id] };
+        });
+        this.throwIfNoData(output.id);
+        var outputData = { array: output, texData: this.texData[output.id] };
+        var key = gpgpu_math.makeShaderKey(program, inputsData, outputData);
+        var binary = this.getAndSaveBinary(key, function () {
+            return gpgpu_math.compileProgram(_this.gpgpu, program, inputsData, outputData);
+        });
+        gpgpu_math.runProgram(binary, inputsData, outputData, customSetup);
+        return output;
+    };
+    MathBackendWebGL.prototype.getAndSaveBinary = function (key, getBinary) {
+        if (!(key in this.binaryCache)) {
+            this.binaryCache[key] = getBinary();
+        }
+        return this.binaryCache[key];
+    };
+    MathBackendWebGL.prototype.getTextureManager = function () {
+        return this.textureManager;
+    };
+    MathBackendWebGL.prototype.dispose = function () {
+        for (var key in this.binaryCache) {
+            this.gpgpu.deleteProgram(this.binaryCache[key].webGLProgram);
+        }
+        this.textureManager.dispose();
+        if (this.gpgpuCreatedLocally) {
+            this.gpgpu.dispose();
+        }
+    };
+    MathBackendWebGL.prototype.throwIfNoData = function (id) {
+        if (!(id in this.texData)) {
+            throw new Error("No data found for NDArray with id " + id + ". " +
+                "Use dl.ENV.math instead of constructing your own NDArrayMath. " +
+                "If you need to construct your own math, make sure this array is " +
+                "allocated after the math construction");
+        }
+    };
+    return MathBackendWebGL;
+}());
+exports.MathBackendWebGL = MathBackendWebGL;
+environment_1.ENV.registerBackend('webgl', function () { return new MathBackendWebGL(); });
+var NDArrayMathGPU = (function (_super) {
+    __extends(NDArrayMathGPU, _super);
+    function NDArrayMathGPU(gpgpu, safeMode) {
+        if (safeMode === void 0) { safeMode = false; }
+        var _this = this;
+        console.warn('new NDArrayMathGPU() is deprecated. Please use the global ' +
+            'dl.ENV.math. In rare cases, to construct your own NDArrayMath ' +
+            'that runs on GPU, use math = new NDArrayMath(\'webgl\', safeMode); ' +
+            'and make sure to set it as global: dl.ENV.setMath(math);');
+        _this = _super.call(this, new MathBackendWebGL(gpgpu), safeMode) || this;
+        environment_1.ENV.setMath(_this);
+        return _this;
+    }
+    NDArrayMathGPU.prototype.getGPGPUContext = function () {
+        return this.backendEngine.getBackend()
+            .getGPGPUContext();
+    };
+    NDArrayMathGPU.prototype.getTextureManager = function () {
+        return this.backendEngine.getBackend()
+            .getTextureManager();
+    };
+    return NDArrayMathGPU;
+}(math_1.NDArrayMath));
+exports.NDArrayMathGPU = NDArrayMathGPU;
+function float32ToTypedArray(a, dtype) {
+    if (dtype === 'float32') {
+        return a;
+    }
+    else if (dtype === 'int32' || dtype === 'bool') {
+        var result = (dtype === 'int32') ? new Int32Array(a.length) :
+            new Uint8Array(a.length);
+        for (var i = 0; i < result.length; ++i) {
+            var val = a[i];
+            val = isNaN(val) ? util.getNaN(dtype) : Math.round(val);
+            result[i] = val;
+        }
+        return result;
+    }
+    else {
+        throw new Error("Unknown dtype " + dtype);
+    }
+}
+function typedArrayToFloat32(a, dtype) {
+    if (a instanceof Float32Array) {
+        return a;
+    }
+    else {
+        var res = new Float32Array(a.length);
+        for (var i = 0; i < res.length; i++) {
+            var val = a[i];
+            res[i] = util.isValNaN(val, dtype) ? NaN : val;
+        }
+        return res;
+    }
+}
+
+},{"../../environment":15,"../../util":101,"../axis_util":54,"../math":94,"../ndarray":95,"../reduce_util":97,"../types":99,"./webgl/argminmax_gpu":62,"./webgl/batchnorm_gpu":63,"./webgl/binaryop_gpu":64,"./webgl/clip_gpu":65,"./webgl/concat_gpu":66,"./webgl/conv_backprop_gpu":67,"./webgl/conv_gpu":68,"./webgl/conv_gpu_depthwise":69,"./webgl/copy_gpu":70,"./webgl/gpgpu_context":71,"./webgl/gpgpu_math":72,"./webgl/gpgpu_util":73,"./webgl/max_pool_backprop_gpu":74,"./webgl/mulmat_gpu":75,"./webgl/multinomial_gpu":76,"./webgl/onehot_gpu":77,"./webgl/pool_gpu":78,"./webgl/reduce_gpu":79,"./webgl/resize_bilinear_gpu":81,"./webgl/slice_gpu":83,"./webgl/tex_util":84,"./webgl/texture_manager":85,"./webgl/tile_gpu":86,"./webgl/transpose_gpu":87,"./webgl/unaryop_gpu":88,"./webgl/webgl_util":89}],58:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var KERNEL_METHODS = {
+    MatMul: function (backend, config) {
+        return backend.matMul(config.inputs.a, config.inputs.b, config.args.aOrientation, config.args.bOrientation);
+    },
+    Clone: function (backend, config) {
+        return backend.clone(config.inputs.x);
+    },
+    Slice1D: function (backend, config) {
+        return backend.slice1D(config.inputs.x, config.args.begin, config.args.size);
+    },
+    Slice2D: function (backend, config) {
+        return backend.slice2D(config.inputs.x, config.args.begin, config.args.size);
+    },
+    Slice3D: function (backend, config) {
+        return backend.slice3D(config.inputs.x, config.args.begin, config.args.size);
+    },
+    Slice4D: function (backend, config) {
+        return backend.slice4D(config.inputs.x, config.args.begin, config.args.size);
+    },
+    Concat1D: function (backend, config) {
+        return backend.concat1D(config.inputs.a, config.inputs.b);
+    },
+    Concat2D: function (backend, config) {
+        return backend.concat2D(config.inputs.a, config.inputs.b, config.args.axis);
+    },
+    Concat3D: function (backend, config) {
+        return backend.concat3D(config.inputs.a, config.inputs.b, config.args.axis);
+    },
+    Concat4D: function (backend, config) {
+        return backend.concat4D(config.inputs.a, config.inputs.b, config.args.axis);
+    },
+    Neg: function (backend, config) {
+        return backend.neg(config.inputs.x);
+    },
+    Add: function (backend, config) {
+        return backend.add(config.inputs.a, config.inputs.b);
+    },
+    Sub: function (backend, config) {
+        return backend.subtract(config.inputs.a, config.inputs.b);
+    },
+    Mul: function (backend, config) {
+        return backend.multiply(config.inputs.a, config.inputs.b);
+    },
+    Div: function (backend, config) {
+        return backend.divide(config.inputs.a, config.inputs.b);
+    },
+    Sum: function (backend, config) {
+        return backend.sum(config.inputs.x, config.args.axes);
+    },
+    ArgMax: function (backend, config) {
+        return backend.argMax(config.inputs.x, config.args.axes);
+    },
+    ArgMin: function (backend, config) {
+        return backend.argMin(config.inputs.x, config.args.axes);
+    },
+    Equal: function (backend, config) {
+        return backend.equal(config.inputs.a, config.inputs.b);
+    },
+    TopKValues: function (backend, config) {
+        return backend.topKValues(config.inputs.x, config.args.k);
+    },
+    TopKIndices: function (backend, config) {
+        return backend.topKIndices(config.inputs.x, config.args.k);
+    },
+    Min: function (backend, config) {
+        return backend.min(config.inputs.x, config.args.axes);
+    },
+    Max: function (backend, config) {
+        return backend.max(config.inputs.x, config.args.axes);
+    },
+    Ceil: function (backend, config) {
+        return backend.ceil(config.inputs.x);
+    },
+    Floor: function (backend, config) {
+        return backend.floor(config.inputs.x);
+    },
+    Pow: function (backend, config) {
+        return backend.pow(config.inputs.a, config.inputs.b);
+    },
+    Exp: function (backend, config) {
+        return backend.exp(config.inputs.x);
+    },
+    Log: function (backend, config) {
+        return backend.log(config.inputs.x);
+    },
+    Sqrt: function (backend, config) {
+        return backend.sqrt(config.inputs.x);
+    },
+    Square: function (backend, config) {
+        return backend.square(config.inputs.x);
+    },
+    Relu: function (backend, config) {
+        return backend.relu(config.inputs.x);
+    },
+    LeakyRelu: function (backend, config) {
+        return backend.leakyRelu(config.inputs.x, config.args.alpha);
+    },
+    PReLU: function (backend, config) {
+        return backend.prelu(config.inputs.x, config.inputs.alpha);
+    },
+    PReLUDer: function (backend, config) {
+        return backend.preluDer(config.inputs.x, config.inputs.alpha);
+    },
+    Elu: function (backend, config) {
+        return backend.elu(config.inputs.x);
+    },
+    EluDer: function (backend, config) {
+        return backend.eluDer(config.inputs.x);
+    },
+    Selu: function (backend, config) {
+        return backend.selu(config.inputs.x);
+    },
+    Abs: function (backend, config) {
+        return backend.abs(config.inputs.x);
+    },
+    Sigmoid: function (backend, config) {
+        return backend.sigmoid(config.inputs.x);
+    },
+    Step: function (backend, config) {
+        return backend.step(config.inputs.x, config.args.alpha);
+    },
+    Sin: function (backend, config) {
+        return backend.sin(config.inputs.x);
+    },
+    Cos: function (backend, config) {
+        return backend.cos(config.inputs.x);
+    },
+    Tan: function (backend, config) {
+        return backend.tan(config.inputs.x);
+    },
+    Asin: function (backend, config) {
+        return backend.asin(config.inputs.x);
+    },
+    Acos: function (backend, config) {
+        return backend.acos(config.inputs.x);
+    },
+    Atan: function (backend, config) {
+        return backend.atan(config.inputs.x);
+    },
+    Sinh: function (backend, config) {
+        return backend.sinh(config.inputs.x);
+    },
+    Cosh: function (backend, config) {
+        return backend.cosh(config.inputs.x);
+    },
+    Tanh: function (backend, config) {
+        return backend.tanh(config.inputs.x);
+    },
+    Clip: function (backend, config) {
+        return backend.clip(config.inputs.x, config.args.min, config.args.max);
+    },
+    Transpose: function (backend, config) {
+        return backend.transpose(config.inputs.x, config.args.perm);
+    },
+    Tile: function (backend, config) {
+        return backend.tile(config.inputs.x, config.args.reps);
+    },
+    Conv2D: function (backend, config) {
+        return backend.conv2d(config.inputs.x, config.inputs.filter, config.inputs.bias, config.args.convInfo);
+    },
+    Conv2DDerInput: function (backend, config) {
+        return backend.conv2dDerInput(config.inputs.dy, config.inputs.filter, config.args.convInfo);
+    },
+    Conv2DDerFilter: function (backend, config) {
+        return backend.conv2dDerFilter(config.inputs.x, config.inputs.dy, config.args.convInfo);
+    },
+    Conv2DDerBias: function (backend, config) {
+        return backend.conv2dDerBias(config.inputs.dy);
+    },
+    DepthwiseConv2D: function (backend, config) {
+        return backend.depthwiseConv2D(config.inputs.x, config.inputs.filter, config.args.convInfo);
+    },
+    MaxPool: function (backend, config) {
+        return backend.maxPool(config.inputs.x, config.args.convInfo);
+    },
+    MaxPoolBackprop: function (backend, config) {
+        return backend.maxPoolBackprop(config.inputs.dy, config.inputs.x, config.args.convInfo);
+    },
+    AvgPool: function (backend, config) {
+        return backend.avgPool(config.inputs.x, config.args.convInfo);
+    },
+    MinPool: function (backend, config) {
+        return backend.minPool(config.inputs.x, config.args.convInfo);
+    },
+    ResizeBilinear3D: function (backend, config) {
+        return backend.resizeBilinear3D(config.inputs.x, config.args.newShape2D, config.args.alignCorners);
+    },
+    BatchNorm3D: function (backend, config) {
+        return backend.batchNormalization3D(config.inputs.x, config.inputs.mean, config.inputs.variance, config.args.varianceEpsilon, config.inputs.scale, config.inputs.offset);
+    },
+    BatchNorm2D: function (backend, config) {
+        return backend.batchNormalization2D(config.inputs.x, config.inputs.mean, config.inputs.variance, config.args.varianceEpsilon, config.inputs.scale, config.inputs.offset);
+    },
+    Multinomial: function (backend, config) {
+        return backend.multinomial(config.inputs.probs, config.args.numSamples, config.args.seed);
+    },
+    OneHot: function (backend, config) {
+        return backend.oneHot(config.inputs.indices, config.args.depth, config.args.onValue, config.args.offValue);
+    }
+};
+function executeKernel(backend, kernelName, config) {
+    return KERNEL_METHODS[kernelName](backend, config);
+}
+exports.executeKernel = executeKernel;
+
+},{}],59:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ndarray_1 = require("../ndarray");
+var tape_util = require("./tape_util");
+var Tape = (function () {
+    function Tape(backend) {
+        this.backend = backend;
+        this.evaluatedTapeNodes = [];
+        this.outputNodeMap = {};
+    }
+    Tape.prototype.addEvaluatedKernelNode = function (node) {
+        this.outputNodeMap[node.output.id] = node;
+        this.evaluatedTapeNodes.push(node);
+    };
+    Tape.prototype.gradientWrt = function (y, xs) {
+        if (this.outputNodeMap[y.id] == null) {
+            throw new Error("Cannot compute gradient: y is not part of this tape.");
+        }
+        var filteredNodes = tape_util.getFilteredNodesXToY(this.evaluatedTapeNodes, xs, y);
+        var arrayAccumulatedGradientMap = {};
+        arrayAccumulatedGradientMap[y.id] = ndarray_1.Scalar.new(1);
+        tape_util.backpropagateGradients(this.backend, arrayAccumulatedGradientMap, filteredNodes);
+        var gradients = [];
+        for (var i = 0; i < xs.length; i++) {
+            gradients.push(arrayAccumulatedGradientMap[xs[i].id]);
+        }
+        return gradients;
+    };
+    return Tape;
+}());
+exports.Tape = Tape;
+
+},{"../ndarray":95,"./tape_util":60}],60:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+function getFilteredNodesXToY(tapeNodes, xs, y) {
+    var arraysFromX = {};
+    for (var i = 0; i < xs.length; i++) {
+        arraysFromX[xs[i].id] = true;
+    }
+    for (var i = 0; i < tapeNodes.length; i++) {
+        var node = tapeNodes[i];
+        var nodeInputs = node.inputAndArgs.inputs;
+        for (var inputName in nodeInputs) {
+            var input = nodeInputs[inputName];
+            for (var j = 0; j < xs.length; j++) {
+                if (arraysFromX[input.id]) {
+                    arraysFromX[node.output.id] = true;
+                    break;
+                }
+            }
+            if (arraysFromX[node.output.id]) {
+                break;
+            }
+        }
+    }
+    var arraysLeadToY = {};
+    arraysLeadToY[y.id] = true;
+    for (var i = tapeNodes.length - 1; i >= 0; i--) {
+        var node = tapeNodes[i];
+        var nodeInputs = node.inputAndArgs.inputs;
+        if (arraysLeadToY[node.output.id]) {
+            for (var inputName in nodeInputs) {
+                arraysLeadToY[nodeInputs[inputName].id] = true;
+            }
+        }
+    }
+    var filteredTapeNodes = [];
+    for (var i = 0; i < tapeNodes.length; i++) {
+        var node = tapeNodes[i];
+        if (arraysFromX[node.output.id] && arraysLeadToY[node.output.id]) {
+            var prunedInputs = {};
+            for (var inputName in node.inputAndArgs.inputs) {
+                var nodeInput = node.inputAndArgs.inputs[inputName];
+                if (arraysFromX[nodeInput.id]) {
+                    prunedInputs[inputName] = nodeInput;
+                }
+            }
+            var prunedNode = Object.assign({}, node);
+            prunedNode.inputAndArgs = { inputs: prunedInputs };
+            filteredTapeNodes.push(prunedNode);
+        }
+    }
+    return filteredTapeNodes;
+}
+exports.getFilteredNodesXToY = getFilteredNodesXToY;
+function backpropagateGradients(backend, arrayAccumulatedGradientMap, filteredNodes) {
+    for (var i = filteredNodes.length - 1; i >= 0; i--) {
+        var node = filteredNodes[i];
+        var dy = arrayAccumulatedGradientMap[node.output.id];
+        if (node.gradient == null) {
+            throw new Error("Cannot compute gradient: gradient function not found for\n              " + node.name + ".");
+        }
+        var inputGradients = node.gradient(dy, node.output);
+        for (var inputName in node.inputAndArgs.inputs) {
+            if (!(inputName in inputGradients)) {
+                throw new Error("Cannot backprop through input " +
+                    (node.name + "." + inputName + ". Gradients found: ") +
+                    (Object.keys(inputGradients) + "."));
+            }
+            var grad = inputGradients[inputName]();
+            var activation = node.inputAndArgs.inputs[inputName];
+            if (arrayAccumulatedGradientMap[activation.id] == null) {
+                arrayAccumulatedGradientMap[activation.id] = grad;
+            }
+            else {
+                var curGradient = arrayAccumulatedGradientMap[activation.id];
+                arrayAccumulatedGradientMap[activation.id] =
+                    backend.add(curGradient, grad);
+                curGradient.dispose();
+            }
+        }
+    }
+}
+exports.backpropagateGradients = backpropagateGradients;
+
+},{}],61:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var MatrixOrientation;
+(function (MatrixOrientation) {
+    MatrixOrientation[MatrixOrientation["REGULAR"] = 0] = "REGULAR";
+    MatrixOrientation[MatrixOrientation["TRANSPOSED"] = 1] = "TRANSPOSED";
+})(MatrixOrientation = exports.MatrixOrientation || (exports.MatrixOrientation = {}));
+
+},{}],62:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ArgMinMaxProgram = (function () {
+    function ArgMinMaxProgram(reduceInfo, op, firstPass) {
+        this.variableNames = ['A'];
+        var windowSize = reduceInfo.windowSize;
+        var batchSize = reduceInfo.batchSize;
+        var inSize = reduceInfo.inSize;
+        var outSize = Math.ceil(inSize / windowSize);
+        if (!firstPass) {
+            this.variableNames.push('bestIndicesA');
+        }
+        this.outputShape = [batchSize, outSize];
+        var compOp = (op === 'max') ? '>' : '<';
+        var indexSnippet = firstPass ?
+            'inOffset + i;' :
+            'round(getBestIndicesA(batch, inOffset + i));';
+        this.userCode = "\n      void main() {\n        ivec2 coords = getOutputCoords();\n        int batch = coords[0];\n        int outIdx = coords[1];\n        int inOffset = outIdx * " + windowSize + ";\n\n        int bestIndex = 0;\n        float bestValue = getA(batch, inOffset);\n\n        for (int i = 0; i < " + windowSize + "; i++) {\n          int inIdx = " + indexSnippet + ";\n          float candidate = getA(batch, inIdx);\n          if (isNaN(candidate)) {\n            setOutput(candidate);\n            return;\n          }\n          if (candidate " + compOp + " bestValue) {\n            bestValue = candidate;\n            bestIndex = inIdx;\n          }\n        }\n        setOutput(float(bestIndex));\n      }\n    ";
+    }
+    return ArgMinMaxProgram;
+}());
+exports.ArgMinMaxProgram = ArgMinMaxProgram;
+
+},{}],63:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var broadcast_util = require("../../broadcast_util");
+var BatchNormProgram = (function () {
+    function BatchNormProgram(xShape, meanShape, varianceShape, offsetShape, scaleShape, varianceEpsilon) {
+        this.outputShape = [];
+        this.supportsBroadcasting = true;
+        this.variableNames = ['x', 'mean', 'variance'];
+        broadcast_util.assertAndGetBroadcastShape(xShape, meanShape);
+        broadcast_util.assertAndGetBroadcastShape(xShape, varianceShape);
+        var offsetSnippet = '0.0';
+        if (offsetShape != null) {
+            broadcast_util.assertAndGetBroadcastShape(xShape, offsetShape);
+            this.variableNames.push('offset');
+            offsetSnippet = 'getOffsetAtOutCoords()';
+        }
+        var scaleSnippet = '1.0';
+        if (scaleShape != null) {
+            broadcast_util.assertAndGetBroadcastShape(xShape, scaleShape);
+            this.variableNames.push('scale');
+            scaleSnippet = 'getScaleAtOutCoords()';
+        }
+        this.outputShape = xShape;
+        this.userCode = "\n      void main() {\n        float x = getXAtOutCoords();\n        float mean = getMeanAtOutCoords();\n        float variance = getVarianceAtOutCoords();\n        float offset = " + offsetSnippet + ";\n        float scale = " + scaleSnippet + ";\n        float inv = scale / sqrt(variance + float(" + varianceEpsilon + "));\n        setOutput((x - mean) * inv + offset);\n      }\n    ";
+    }
+    return BatchNormProgram;
+}());
+exports.BatchNormProgram = BatchNormProgram;
+
+},{"../../broadcast_util":90}],64:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var broadcast_util = require("../../broadcast_util");
+exports.ADD = 'return a + b;';
+exports.SUB = 'return a - b;';
+exports.MUL = 'return a * b;';
+exports.DIV = 'return a / b;';
+exports.POW = "\n  return (round(mod(b, 2.0)) == 0 || round(mod(b, 2.0)) == 2) ?\n      pow(abs(a), b) : sign(a) * pow(abs(a), b);\n";
+exports.EQUAL = "\n  if (isNaN(a)) return a;\n  if (isNaN(b)) return b;\n  return float(a == b);\n";
+exports.PRELU = "\n  return (a >= 0.0) ? a : b * a;\n";
+exports.PRELU_DER = "\n  return (a > 0.0) ? 1.0 : ((a < 0.0) ? b : a);\n";
+var BinaryOpProgram = (function () {
+    function BinaryOpProgram(op, aShape, bShape) {
+        this.variableNames = ['A', 'B'];
+        this.supportsBroadcasting = true;
+        this.outputShape =
+            broadcast_util.assertAndGetBroadcastShape(aShape, bShape);
+        this.userCode = "\n      float binaryOperation(float a, float b) {\n        " + op + "\n      }\n\n      void main() {\n        float a = getAAtOutCoords();\n        float b = getBAtOutCoords();\n        setOutput(binaryOperation(a, b));\n      }\n    ";
+    }
+    return BinaryOpProgram;
+}());
+exports.BinaryOpProgram = BinaryOpProgram;
+
+},{"../../broadcast_util":90}],65:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ClipProgram = (function () {
+    function ClipProgram(aShape, min, max) {
+        this.variableNames = ['A'];
+        this.outputShape = aShape;
+        var minFixed = min.toFixed(20);
+        var maxFixed = max.toFixed(20);
+        this.userCode = "\n      void main() {\n        float value = getAAtOutCoords();\n        if (isNaN(value)) {\n          setOutput(value);\n          return;\n        }\n\n        setOutput(clamp(value, " + minFixed + ", " + maxFixed + "));\n      }\n    ";
+    }
+    return ClipProgram;
+}());
+exports.ClipProgram = ClipProgram;
+
+},{}],66:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var concat_util = require("../../concat_util");
+var shader_compiler_1 = require("./shader_compiler");
+var ConcatProgram = (function () {
+    function ConcatProgram(aShape, bShape, axis) {
+        this.variableNames = ['A', 'B'];
+        this.outputShape = [];
+        var yAxes = ['yR', 'yC', 'yD', 'yW'];
+        var concatAxis = yAxes[axis];
+        this.outputShape = concat_util.computeOutShape(aShape, bShape, axis);
+        var dType = shader_compiler_1.getCoordsDataType(aShape.length);
+        var unpackSnippet = getUnpack(aShape.length);
+        var sampleCoords = getSampleCoords(aShape.length);
+        this.userCode = "\n      void main() {\n        " + dType + " coords = getOutputCoords();\n        " + unpackSnippet + "\n\n        float value = 0.0;\n        if (" + concatAxis + " < " + aShape[axis] + ") {\n          value = getA(" + sampleCoords + ");\n        } else {\n          " + concatAxis + " -= " + aShape[axis] + ";\n          value = getB(" + sampleCoords + ");\n        }\n\n        setOutput(value);\n      }\n    ";
+    }
+    return ConcatProgram;
+}());
+exports.ConcatProgram = ConcatProgram;
+function getSampleCoords(rank) {
+    if (rank === 1) {
+        return 'yR';
+    }
+    else if (rank === 2) {
+        return 'yR, yC';
+    }
+    else if (rank === 3) {
+        return 'yR, yC, yD';
+    }
+    else if (rank === 4) {
+        return 'yR, yC, yD, yW';
+    }
+    else {
+        throw Error("Concat for rank " + rank + " is not yet supported");
+    }
+}
+function getUnpack(rank) {
+    var res = rank === 1 ? 'int yR = coords;' : 'int yR = coords.x;';
+    if (rank > 1) {
+        res += '\nint yC = coords.y;';
+    }
+    if (rank > 2) {
+        res += '\nint yD = coords.z;';
+    }
+    if (rank > 3) {
+        res += '\nint yW = coords.w;';
+    }
+    if (rank > 4) {
+        throw Error("Concat for rank " + rank + " is not yet supported");
+    }
+    return res;
+}
+
+},{"../../concat_util":91,"./shader_compiler":82}],67:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var Conv2DDerFilterProgram = (function () {
+    function Conv2DDerFilterProgram(convInfo) {
+        this.variableNames = ['x', 'dy'];
+        this.outputShape = convInfo.filterShape;
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var padTop = convInfo.padInfo.top;
+        var padLeft = convInfo.padInfo.left;
+        this.userCode = "\n      void main() {\n        ivec4 coords = getOutputCoords();\n        int wR = coords.x;\n        int wC = coords.y;\n        int d1 = coords.z;\n        int d2 = coords.w;\n\n        // Convolve x(?, ?, d1) with dy(:, :, d2) to get dw(wR, wC, d1, d2).\n        // ? = to be determined. : = across all values in that axis.\n        float dotProd = 0.0;\n\n        for (int b = 0; b < " + convInfo.batchSize + "; b++) {\n          for (int yR = 0; yR < " + convInfo.outHeight + "; yR++) {\n            int xR = wR + yR * " + strideHeight + " - " + padTop + ";\n\n            if (xR < 0 || xR >= " + convInfo.inHeight + ") {\n              continue;\n            }\n\n            for (int yC = 0; yC < " + convInfo.outWidth + "; yC++) {\n              int xC = wC + yC * " + strideWidth + " - " + padLeft + ";\n\n              if (xC < 0 || xC >= " + convInfo.inWidth + ") {\n                continue;\n              }\n\n              float dyValue = getDy(b, yR, yC, d2);\n              float xValue = getX(b, xR, xC, d1);\n              dotProd += (xValue * dyValue);\n            }\n          }\n        }\n        setOutput(dotProd);\n      }\n    ";
+    }
+    return Conv2DDerFilterProgram;
+}());
+exports.Conv2DDerFilterProgram = Conv2DDerFilterProgram;
+var Conv2DDerInputProgram = (function () {
+    function Conv2DDerInputProgram(convInfo) {
+        this.variableNames = ['dy', 'W'];
+        this.outputShape = convInfo.inShape;
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var padTop = filterHeight - 1 - convInfo.padInfo.top;
+        var padLeft = filterWidth - 1 - convInfo.padInfo.left;
+        this.userCode = "\n      const ivec2 pads = ivec2(" + padTop + ", " + padLeft + ");\n\n      void main() {\n        ivec4 coords = getOutputCoords();\n        int batch = coords[0];\n        int d1 = coords[3];\n\n        ivec2 dyCorner = coords.yz - pads;\n        int dyRCorner = dyCorner.x;\n        int dyCCorner = dyCorner.y;\n\n        // Convolve dy(?, ?, d2) with w(:, :, d1, d2) to compute dx(xR, xC, d1).\n        // ? = to be determined. : = across all values in that axis.\n        float dotProd = 0.0;\n        for (int wR = 0; wR < " + filterHeight + "; wR++) {\n          float dyR = float(dyRCorner + wR) / " + strideHeight + ".0;\n\n          if (dyR < 0.0 || dyR >= " + convInfo.outHeight + ".0 || fract(dyR) > 0.0) {\n            continue;\n          }\n          int idyR = int(dyR);\n\n          int wRPerm = " + filterHeight + " - 1 - wR;\n\n          for (int wC = 0; wC < " + filterWidth + "; wC++) {\n            float dyC = float(dyCCorner + wC) / " + strideWidth + ".0;\n\n            if (dyC < 0.0 || dyC >= " + convInfo.outWidth + ".0 ||\n                fract(dyC) > 0.0) {\n              continue;\n            }\n            int idyC = int(dyC);\n\n            int wCPerm = " + filterWidth + " - 1 - wC;\n\n            for (int d2 = 0; d2 < " + convInfo.outChannels + "; d2++) {\n              float xValue = getDy(batch, idyR, idyC, d2);\n              float wValue = getW(wRPerm, wCPerm, d1, d2);\n              dotProd += xValue * wValue;\n            }\n          }\n        }\n        setOutput(dotProd);\n      }\n    ";
+    }
+    return Conv2DDerInputProgram;
+}());
+exports.Conv2DDerInputProgram = Conv2DDerInputProgram;
+var Conv2DDerBiasProgram = (function () {
+    function Conv2DDerBiasProgram(yShape) {
+        this.variableNames = ['dy'];
+        var batchSize = yShape[0], yNumRows = yShape[1], yNumCols = yShape[2], outputDepth = yShape[3];
+        this.outputShape = [outputDepth];
+        this.userCode = "\n      void main() {\n        int d2 = getOutputCoords();\n\n        float derBias = 0.0;\n        for (int b = 0; b < " + batchSize + "; b++) {\n          for (int yR = 0; yR < " + yNumRows + "; yR++) {\n            for (int yC = 0; yC < " + yNumCols + "; yC++) {\n              derBias += getDy(b, yR, yC, d2);\n            }\n          }\n        }\n        setOutput(derBias);\n      }\n    ";
+    }
+    return Conv2DDerBiasProgram;
+}());
+exports.Conv2DDerBiasProgram = Conv2DDerBiasProgram;
+
+},{}],68:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var Conv2DProgram = (function () {
+    function Conv2DProgram(convInfo, hasBias) {
+        this.variableNames = ['x', 'W'];
+        if (hasBias) {
+            this.variableNames.push('bias');
+        }
+        this.outputShape = convInfo.outShape;
+        var biasSnippet = hasBias ? 'dotProd += getBias(d2);' : '';
+        var padTop = convInfo.padInfo.top;
+        var padLeft = convInfo.padInfo.left;
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var inputDepthNearestVec4 = Math.floor(convInfo.inChannels / 4) * 4;
+        var inputDepthVec4Remainder = convInfo.inChannels % 4;
+        this.userCode = "\n      const ivec2 strides = ivec2(" + strideHeight + ", " + strideWidth + ");\n      const ivec2 pads = ivec2(" + padTop + ", " + padLeft + ");\n\n      void main() {\n        ivec4 coords = getOutputCoords();\n        int batch = coords[0];\n        int d2 = coords[3];\n\n        ivec2 xRCCorner = coords.yz * strides - pads;\n        int xRCorner = xRCCorner.x;\n        int xCCorner = xRCCorner.y;\n\n        // Convolve x(?, ?, d1) with w(:, :, d1, d2) to get y(yR, yC, d2).\n        // ? = to be determined. : = across all values in that axis.\n        float dotProd = 0.0;\n        for (int wR = 0; wR < " + filterHeight + "; wR++) {\n          int xR = xRCorner + wR;\n\n          if (xR < 0 || xR >= " + convInfo.inHeight + ") {\n            continue;\n          }\n\n          for (int wC = 0; wC < " + filterWidth + "; wC++) {\n            int xC = xCCorner + wC;\n\n            if (xC < 0 || xC >= " + convInfo.inWidth + ") {\n              continue;\n            }\n\n            for (int d1 = 0; d1 < " + inputDepthNearestVec4 + "; d1 += 4) {\n              vec4 xValues = vec4(\n                getX(batch, xR, xC, d1),\n                getX(batch, xR, xC, d1 + 1),\n                getX(batch, xR, xC, d1 + 2),\n                getX(batch, xR, xC, d1 + 3)\n              );\n              vec4 wValues = vec4(\n                getW(wR, wC, d1, d2),\n                getW(wR, wC, d1 + 1, d2),\n                getW(wR, wC, d1 + 2, d2),\n                getW(wR, wC, d1 + 3, d2)\n              );\n\n              dotProd += dot(xValues, wValues);\n            }\n\n            if (" + (inputDepthVec4Remainder === 1) + ") {\n              dotProd +=\n                getX(batch, xR, xC, " + inputDepthNearestVec4 + ") *\n                getW(wR, wC, " + inputDepthNearestVec4 + ", d2);\n            } else if (" + (inputDepthVec4Remainder === 2) + ") {\n              vec2 xValues = vec2(\n                getX(batch, xR, xC, " + inputDepthNearestVec4 + "),\n                getX(batch, xR, xC, " + inputDepthNearestVec4 + " + 1)\n              );\n              vec2 wValues = vec2(\n                getW(wR, wC, " + inputDepthNearestVec4 + ", d2),\n                getW(wR, wC, " + inputDepthNearestVec4 + " + 1, d2)\n              );\n              dotProd += dot(xValues, wValues);\n            } else if (" + (inputDepthVec4Remainder === 3) + ") {\n              vec3 xValues = vec3(\n                getX(batch, xR, xC, " + inputDepthNearestVec4 + "),\n                getX(batch, xR, xC, " + inputDepthNearestVec4 + " + 1),\n                getX(batch, xR, xC, " + inputDepthNearestVec4 + " + 2)\n              );\n              vec3 wValues = vec3(\n                getW(wR, wC, " + inputDepthNearestVec4 + ", d2),\n                getW(wR, wC, " + inputDepthNearestVec4 + " + 1, d2),\n                getW(wR, wC, " + inputDepthNearestVec4 + " + 2, d2)\n              );\n              dotProd += dot(xValues, wValues);\n            }\n          }\n        }\n        " + biasSnippet + "\n        setOutput(dotProd);\n      }\n    ";
+    }
+    return Conv2DProgram;
+}());
+exports.Conv2DProgram = Conv2DProgram;
+
+},{}],69:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var DepthwiseConv2DProgram = (function () {
+    function DepthwiseConv2DProgram(convInfo) {
+        this.variableNames = ['x', 'W'];
+        this.outputShape = convInfo.outShape;
+        var xNumRows = convInfo.inHeight;
+        var xNumCols = convInfo.inWidth;
+        var padTop = convInfo.padInfo.top;
+        var padLeft = convInfo.padInfo.left;
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var channelMul = convInfo.outChannels / convInfo.inChannels;
+        this.userCode = "\n      const ivec2 strides = ivec2(" + strideHeight + ", " + strideWidth + ");\n      const ivec2 pads = ivec2(" + padTop + ", " + padLeft + ");\n\n      void main() {\n        ivec4 coords = getOutputCoords();\n        int batch = coords.x;\n        ivec2 xRCCorner = coords.yz * strides - pads;\n        int d2 = coords.w;\n        int d1 = d2 / " + channelMul + ";\n        int q = d2 - d1 * " + channelMul + ";\n\n        int xRCorner = xRCCorner.x;\n        int xCCorner = xRCCorner.y;\n\n        // Convolve x(?, ?, d1) with w(:, :, d1, q) to get y(yR, yC, d2).\n        // ? = to be determined. : = across all values in that axis.\n        float dotProd = 0.0;\n        // TODO(dsmilkov): Flatten the two for loops and vec4 the operations.\n        for (int wR = 0; wR < " + filterHeight + "; wR++) {\n          int xR = xRCorner + wR;\n\n          if (xR < 0 || xR >= " + xNumRows + ") {\n            continue;\n          }\n\n          for (int wC = 0; wC < " + filterWidth + "; wC++) {\n            int xC = xCCorner + wC;\n\n            if (xC < 0 || xC >= " + xNumCols + ") {\n              continue;\n            }\n\n            float xVal = getX(batch, xR, xC, d1);\n            float wVal = getW(wR, wC, d1, q);\n            dotProd += xVal * wVal;\n          }\n        }\n        setOutput(dotProd);\n      }\n    ";
+    }
+    return DepthwiseConv2DProgram;
+}());
+exports.DepthwiseConv2DProgram = DepthwiseConv2DProgram;
+
+},{}],70:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var Copy2DProgram = (function () {
+    function Copy2DProgram(srcNumCols, destNumCols) {
+        this.variableNames = ['source'];
+        this.outputShape = null;
+        this.userCode = "\n      uniform ivec2 sourceStart;\n      uniform ivec2 destStart;\n\n      void main() {\n        ivec2 destCoords = getOutputCoords() - destStart;\n        int index = destCoords.x * " + destNumCols + " + destCoords.y;\n        int r = index / " + srcNumCols + ";\n        ivec2 sourceCoords = sourceStart + ivec2(r, index - r * " + srcNumCols + ");\n        setOutput(getSource(sourceCoords.x, sourceCoords.y));\n      }\n    ";
+    }
+    Copy2DProgram.prototype.getCustomSetupFunc = function (sourceStart, destStart, destSize) {
+        return function (gpgpu, webGLProgram) {
+            gpgpu.setOutputMatrixWriteRegion(destStart[0], destSize[0], destStart[1], destSize[1]);
+            var sourceStartCRLoc = gpgpu.getUniformLocation(webGLProgram, 'sourceStart');
+            gpgpu.gl.uniform2i(sourceStartCRLoc, sourceStart[0], sourceStart[1]);
+            var destStartCRLoc = gpgpu.getUniformLocation(webGLProgram, 'destStart');
+            gpgpu.gl.uniform2i(destStartCRLoc, destStart[0], destStart[1]);
+        };
+    };
+    return Copy2DProgram;
+}());
+exports.Copy2DProgram = Copy2DProgram;
+
+},{}],71:[function(require,module,exports){
+"use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __generator = (this && this.__generator) || function (thisArg, body) {
+    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
+    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
+    function verb(n) { return function (v) { return step([n, v]); }; }
+    function step(op) {
+        if (f) throw new TypeError("Generator is already executing.");
+        while (_) try {
+            if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
+            if (y = 0, t) op = [0, t.value];
+            switch (op[0]) {
+                case 0: case 1: t = op; break;
+                case 4: _.label++; return { value: op[1], done: false };
+                case 5: _.label++; y = op[1]; op = [0]; continue;
+                case 7: op = _.ops.pop(); _.trys.pop(); continue;
+                default:
+                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
+                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
+                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
+                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
+                    if (t[2]) _.ops.pop();
+                    _.trys.pop(); continue;
+            }
+            op = body.call(thisArg, _);
+        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
+        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
+    }
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../../environment");
+var util = require("../../../util");
+var gpgpu_util = require("./gpgpu_util");
+var tex_util = require("./tex_util");
+var webgl_util = require("./webgl_util");
+var GPGPUContext = (function () {
+    function GPGPUContext(gl) {
+        this.outputTexture = null;
+        this.program = null;
+        this.disposed = false;
+        this.autoDebugValidate = false;
+        if (gl != null) {
+            this.gl = gl;
+        }
+        else {
+            this.gl = gpgpu_util.createWebGLContext();
+        }
+        if (environment_1.ENV.get('WEBGL_VERSION') === 1) {
+            this.textureFloatExtension =
+                webgl_util.getExtensionOrThrow(this.gl, 'OES_texture_float');
+            this.colorBufferFloatExtension =
+                this.gl.getExtension('WEBGL_color_buffer_float');
+        }
+        else {
+            this.colorBufferFloatExtension =
+                webgl_util.getExtensionOrThrow(this.gl, 'EXT_color_buffer_float');
+        }
+        this.loseContextExtension =
+            webgl_util.getExtensionOrThrow(this.gl, 'WEBGL_lose_context');
+        if (environment_1.ENV.get('WEBGL_GET_BUFFER_SUB_DATA_ASYNC_EXTENSION_ENABLED')) {
+            this.getBufferSubDataAsyncExtension =
+                this.gl.getExtension('WEBGL_get_buffer_sub_data_async');
+        }
+        this.vertexBuffer = gpgpu_util.createVertexBuffer(this.gl);
+        this.indexBuffer = gpgpu_util.createIndexBuffer(this.gl);
+        this.framebuffer = webgl_util.createFramebuffer(this.gl);
+    }
+    GPGPUContext.prototype.dispose = function () {
+        var _this = this;
+        this.throwIfDisposed();
+        if (this.program != null) {
+            console.warn('Disposing a GPGPUContext that still has a bound WebGLProgram.' +
+                ' This is probably a resource leak, delete the program with ' +
+                'GPGPUContext.deleteProgram before disposing.');
+        }
+        if (this.outputTexture != null) {
+            console.warn('Disposing a GPGPUContext that still has a bound output matrix ' +
+                'texture.  This is probably a resource leak, delete the output ' +
+                'matrix texture with GPGPUContext.deleteMatrixTexture before ' +
+                'disposing.');
+        }
+        var gl = this.gl;
+        webgl_util.callAndCheck(gl, function () { return gl.finish(); });
+        webgl_util.callAndCheck(gl, function () { return gl.bindFramebuffer(gl.FRAMEBUFFER, null); });
+        webgl_util.callAndCheck(gl, function () { return gl.deleteFramebuffer(_this.framebuffer); });
+        webgl_util.callAndCheck(gl, function () { return gl.bindBuffer(gl.ARRAY_BUFFER, null); });
+        webgl_util.callAndCheck(gl, function () { return gl.deleteBuffer(_this.vertexBuffer); });
+        webgl_util.callAndCheck(gl, function () { return gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null); });
+        webgl_util.callAndCheck(gl, function () { return gl.deleteBuffer(_this.indexBuffer); });
+        this.loseContextExtension.loseContext();
+        this.disposed = true;
+    };
+    GPGPUContext.prototype.enableAutomaticDebugValidation = function (enabled) {
+        this.autoDebugValidate = enabled;
+        webgl_util.enableDebugWebGLErrorChecking(enabled);
+    };
+    GPGPUContext.prototype.createMatrixTexture = function (rows, columns) {
+        this.throwIfDisposed();
+        return gpgpu_util.createMatrixTexture(this.gl, rows, columns);
+    };
+    GPGPUContext.prototype.uploadPixelDataToTexture = function (texture, pixels) {
+        this.throwIfDisposed();
+        gpgpu_util.uploadPixelDataToTexture(this.gl, texture, pixels);
+    };
+    GPGPUContext.prototype.createPackedMatrixTexture = function (rows, columns) {
+        this.throwIfDisposed();
+        return gpgpu_util.createPackedMatrixTexture(this.gl, rows, columns);
+    };
+    GPGPUContext.prototype.deleteMatrixTexture = function (texture) {
+        var _this = this;
+        this.throwIfDisposed();
+        if (this.outputTexture === texture) {
+            webgl_util.unbindColorTextureFromFramebuffer(this.gl, this.framebuffer);
+            this.outputTexture = null;
+        }
+        webgl_util.callAndCheck(this.gl, function () { return _this.gl.deleteTexture(texture); });
+    };
+    GPGPUContext.prototype.uploadMatrixToTexture = function (texture, rows, columns, matrix) {
+        this.throwIfDisposed();
+        var numChannels = 1;
+        return gpgpu_util.uploadMatrixToTexture(this.gl, texture, rows, columns, matrix, numChannels);
+    };
+    GPGPUContext.prototype.uploadMatrixToPackedTexture = function (texture, rows, columns, matrix) {
+        this.throwIfDisposed();
+        return gpgpu_util.uploadMatrixToPackedTexture(this.gl, texture, rows, columns, matrix);
+    };
+    GPGPUContext.prototype.downloadMatrixFromTexture = function (texture, rows, columns) {
+        var _this = this;
+        return this.downloadMatrixDriver(texture, function () {
+            return gpgpu_util.downloadMatrixFromOutputTexture(_this.gl, rows, columns);
+        });
+    };
+    GPGPUContext.prototype.downloadMatrixFromTextureAsync = function (texture, rows, columns) {
+        return __awaiter(this, void 0, void 0, function () {
+            var _this = this;
+            return __generator(this, function (_a) {
+                if (this.getBufferSubDataAsyncExtension == null) {
+                    throw new Error("Cannot download matrix from output texture asynchronously, " +
+                        "WEBGL_get_buffer_sub_data_async is not enabled.");
+                }
+                return [2, this.downloadMatrixDriverAsync(texture, function () { return gpgpu_util.downloadMatrixFromOutputTextureAsync(_this.gl, _this.getBufferSubDataAsyncExtension, rows, columns); })];
+            });
+        });
+    };
+    GPGPUContext.prototype.downloadMatrixFromRGBAColorTexture = function (texture, rows, columns, channels) {
+        var _this = this;
+        return this.downloadMatrixDriver(texture, function () { return gpgpu_util.downloadMatrixFromRGBAColorTexture(_this.gl, rows, columns, channels); });
+    };
+    GPGPUContext.prototype.downloadMatrixFromPackedTexture = function (texture, rows, columns) {
+        var _this = this;
+        return this.downloadMatrixDriver(texture, function () { return gpgpu_util.downloadMatrixFromPackedOutputTexture(_this.gl, rows, columns); });
+    };
+    GPGPUContext.prototype.createProgram = function (fragmentShaderSource) {
+        this.throwIfDisposed();
+        var gl = this.gl;
+        var fragmentShader = webgl_util.createFragmentShader(gl, fragmentShaderSource);
+        var vertexShader = gpgpu_util.createVertexShader(gl);
+        var program = webgl_util.createProgram(gl);
+        webgl_util.callAndCheck(gl, function () { return gl.attachShader(program, vertexShader); });
+        webgl_util.callAndCheck(gl, function () { return gl.attachShader(program, fragmentShader); });
+        webgl_util.linkProgram(gl, program);
+        if (this.autoDebugValidate) {
+            webgl_util.validateProgram(gl, program);
+        }
+        return program;
+    };
+    GPGPUContext.prototype.deleteProgram = function (program) {
+        var _this = this;
+        this.throwIfDisposed();
+        if (program === this.program) {
+            this.program = null;
+        }
+        if (program != null) {
+            webgl_util.callAndCheck(this.gl, function () { return _this.gl.deleteProgram(program); });
+        }
+    };
+    GPGPUContext.prototype.setProgram = function (program) {
+        var _this = this;
+        this.throwIfDisposed();
+        this.program = program;
+        if ((this.program != null) && this.autoDebugValidate) {
+            webgl_util.validateProgram(this.gl, this.program);
+        }
+        webgl_util.callAndCheck(this.gl, function () { return _this.gl.useProgram(program); });
+    };
+    GPGPUContext.prototype.getUniformLocation = function (program, uniformName) {
+        this.throwIfDisposed();
+        return webgl_util.getProgramUniformLocationOrThrow(this.gl, program, uniformName);
+    };
+    GPGPUContext.prototype.getAttributeLocation = function (program, attribute) {
+        var _this = this;
+        this.throwIfDisposed();
+        return webgl_util.callAndCheck(this.gl, function () { return _this.gl.getAttribLocation(program, attribute); });
+    };
+    GPGPUContext.prototype.getUniformLocationNoThrow = function (program, uniformName) {
+        this.throwIfDisposed();
+        return this.gl.getUniformLocation(program, uniformName);
+    };
+    GPGPUContext.prototype.setInputMatrixTexture = function (inputMatrixTexture, uniformLocation, textureUnit) {
+        this.throwIfDisposed();
+        this.throwIfNoProgram();
+        webgl_util.bindTextureToProgramUniformSampler(this.gl, this.program, inputMatrixTexture, uniformLocation, textureUnit);
+    };
+    GPGPUContext.prototype.setOutputMatrixTexture = function (outputMatrixTexture, rows, columns) {
+        this.setOutputMatrixTextureDriver(outputMatrixTexture, columns, rows);
+    };
+    GPGPUContext.prototype.setOutputPackedMatrixTexture = function (outputPackedMatrixTexture, rows, columns) {
+        this.throwIfDisposed();
+        var _a = tex_util.getPackedMatrixTextureShapeWidthHeight(rows, columns), width = _a[0], height = _a[1];
+        this.setOutputMatrixTextureDriver(outputPackedMatrixTexture, width, height);
+    };
+    GPGPUContext.prototype.setOutputMatrixWriteRegion = function (startRow, numRows, startColumn, numColumns) {
+        this.setOutputMatrixWriteRegionDriver(startColumn, startRow, numColumns, numRows);
+    };
+    GPGPUContext.prototype.setOutputPackedMatrixWriteRegion = function (startRow, numRows, startColumn, numColumns) {
+        throw new Error('setOutputPackedMatrixWriteRegion not implemented.');
+    };
+    GPGPUContext.prototype.debugValidate = function () {
+        if (this.program != null) {
+            webgl_util.validateProgram(this.gl, this.program);
+        }
+        webgl_util.validateFramebuffer(this.gl);
+    };
+    GPGPUContext.prototype.executeProgram = function (attribLocations) {
+        this.throwIfDisposed();
+        this.throwIfNoProgram();
+        var gl = this.gl;
+        gpgpu_util.bindVertexProgramAttributeStreams(gl, this.program, this.vertexBuffer, attribLocations);
+        if (this.autoDebugValidate) {
+            this.debugValidate();
+        }
+        webgl_util.callAndCheck(gl, function () { return gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); });
+    };
+    GPGPUContext.prototype.blockUntilAllProgramsCompleted = function () {
+        var _this = this;
+        this.throwIfDisposed();
+        webgl_util.callAndCheck(this.gl, function () { return _this.gl.finish(); });
+    };
+    GPGPUContext.prototype.runQuery = function (queryFn) {
+        if (environment_1.ENV.get('WEBGL_VERSION') === 2) {
+            return this.runQueryWebGL2(queryFn);
+        }
+        return this.runQueryWebGL1(queryFn);
+    };
+    GPGPUContext.prototype.runQueryWebGL2 = function (benchmark) {
+        var _this = this;
+        var ext = webgl_util.getExtensionOrThrow(this.gl, 'EXT_disjoint_timer_query_webgl2');
+        var query = this.gl.createQuery();
+        this.gl.beginQuery(ext.TIME_ELAPSED_EXT, query);
+        benchmark();
+        this.gl.endQuery(ext.TIME_ELAPSED_EXT);
+        return new Promise(function (resolve, reject) {
+            var queryGPU = function () {
+                var available = _this.gl
+                    .getQueryParameter(query, _this.gl.QUERY_RESULT_AVAILABLE);
+                var disjoint = _this.gl.getParameter(ext.GPU_DISJOINT_EXT);
+                return available && !disjoint;
+            };
+            var getTimeElapsed = function () {
+                var timeElapsedNanos = _this.gl
+                    .getQueryParameter(query, _this.gl.QUERY_RESULT);
+                resolve(timeElapsedNanos / 1000000);
+            };
+            var resolveWithWarning = function () {
+                console.warn('Disjoint query timer never available.');
+                resolve(-1);
+            };
+            util.repeatedTry(queryGPU).then(getTimeElapsed).catch(resolveWithWarning);
+        });
+    };
+    GPGPUContext.prototype.runQueryWebGL1 = function (benchmark) {
+        var _this = this;
+        var ext = webgl_util.getExtensionOrThrow(this.gl, 'EXT_disjoint_timer_query');
+        var query = ext.createQueryEXT();
+        ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query);
+        benchmark();
+        ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
+        return new Promise(function (resolve, reject) {
+            var queryGPU = function () {
+                var available = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT);
+                var disjoint = _this.gl.getParameter(ext.GPU_DISJOINT_EXT);
+                return available && !disjoint;
+            };
+            var getTimeElapsed = function () {
+                var timeElapsedNanos = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT);
+                resolve(timeElapsedNanos / 1000000);
+            };
+            var resolveWithWarning = function () {
+                console.warn('Disjoint query timer never available.');
+                resolve(-1);
+            };
+            util.repeatedTry(queryGPU).then(getTimeElapsed).catch(resolveWithWarning);
+        });
+    };
+    GPGPUContext.prototype.downloadMatrixDriverSetup = function (texture) {
+        this.throwIfDisposed();
+        webgl_util.bindColorTextureToFramebuffer(this.gl, texture, this.framebuffer);
+        if (this.autoDebugValidate) {
+            webgl_util.validateFramebuffer(this.gl);
+        }
+    };
+    GPGPUContext.prototype.downloadMatrixDriverTeardown = function () {
+        if (this.outputTexture != null) {
+            webgl_util.bindColorTextureToFramebuffer(this.gl, this.outputTexture, this.framebuffer);
+            if (this.autoDebugValidate) {
+                webgl_util.validateFramebuffer(this.gl);
+            }
+        }
+        else {
+            webgl_util.unbindColorTextureFromFramebuffer(this.gl, this.framebuffer);
+        }
+    };
+    GPGPUContext.prototype.downloadMatrixDriver = function (texture, downloadAndDecode) {
+        this.downloadMatrixDriverSetup(texture);
+        var result = downloadAndDecode();
+        this.downloadMatrixDriverTeardown();
+        return result;
+    };
+    GPGPUContext.prototype.downloadMatrixDriverAsync = function (texture, downloadAndDecode) {
+        return __awaiter(this, void 0, void 0, function () {
+            var result;
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0:
+                        this.downloadMatrixDriverSetup(texture);
+                        return [4, downloadAndDecode()];
+                    case 1:
+                        result = _a.sent();
+                        this.downloadMatrixDriverTeardown();
+                        return [2, result];
+                }
+            });
+        });
+    };
+    GPGPUContext.prototype.setOutputMatrixTextureDriver = function (outputMatrixTextureMaybePacked, width, height) {
+        this.throwIfDisposed();
+        var gl = this.gl;
+        webgl_util.bindColorTextureToFramebuffer(gl, outputMatrixTextureMaybePacked, this.framebuffer);
+        if (this.autoDebugValidate) {
+            webgl_util.validateFramebuffer(gl);
+        }
+        this.outputTexture = outputMatrixTextureMaybePacked;
+        webgl_util.callAndCheck(gl, function () { return gl.viewport(0, 0, width, height); });
+        webgl_util.callAndCheck(gl, function () { return gl.scissor(0, 0, width, height); });
+    };
+    GPGPUContext.prototype.setOutputMatrixWriteRegionDriver = function (x, y, width, height) {
+        var _this = this;
+        this.throwIfDisposed();
+        webgl_util.callAndCheck(this.gl, function () { return _this.gl.scissor(x, y, width, height); });
+    };
+    GPGPUContext.prototype.throwIfDisposed = function () {
+        if (this.disposed) {
+            throw new Error('Attempted to use disposed GPGPUContext.');
+        }
+    };
+    GPGPUContext.prototype.throwIfNoProgram = function () {
+        if (this.program == null) {
+            throw new Error('No GPU program is currently set.');
+        }
+    };
+    return GPGPUContext;
+}());
+exports.GPGPUContext = GPGPUContext;
+
+},{"../../../environment":15,"../../../util":101,"./gpgpu_util":73,"./tex_util":84,"./webgl_util":89}],72:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../../environment");
+var util = require("../../../util");
+var shader_compiler = require("./shader_compiler");
+var ATTRIBUTE_NAMES = ['uv', 'clipSpacePos'];
+var NAN_UNIFORM_NAME = 'NaN';
+function shouldUploadNaNUniform() {
+    return !environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED');
+}
+function compileProgram(gpgpu, program, inputs, output) {
+    var userCode = program.userCode;
+    var inputInfos = inputs.map(function (input, i) {
+        var shapeInfo = {
+            logicalShape: input.array.shape,
+            texShape: input.texData.texShape,
+            textureType: input.texData.textureType
+        };
+        return { name: program.variableNames[i], shapeInfo: shapeInfo };
+    });
+    var inShapeInfos = inputInfos.map(function (x) { return x.shapeInfo; });
+    var outShapeInfo = {
+        logicalShape: output.array.shape,
+        texShape: output.texData.texShape,
+        textureType: output.texData.textureType
+    };
+    var source = shader_compiler.makeShader(inputInfos, outShapeInfo, userCode, program.supportsBroadcasting === true);
+    var webGLProgram = gpgpu.createProgram(source);
+    var uniformLocations = {};
+    for (var i = 0; i < program.variableNames.length; i++) {
+        var uniformName = program.variableNames[i];
+        uniformLocations[uniformName] =
+            gpgpu.getUniformLocation(webGLProgram, uniformName);
+    }
+    var attributeLocations = {};
+    ATTRIBUTE_NAMES.forEach(function (attribute) {
+        attributeLocations[attribute] =
+            gpgpu.getAttributeLocation(webGLProgram, attribute);
+    });
+    if (shouldUploadNaNUniform()) {
+        uniformLocations[NAN_UNIFORM_NAME] =
+            gpgpu.getUniformLocation(webGLProgram, NAN_UNIFORM_NAME);
+    }
+    return {
+        program: program,
+        source: source,
+        webGLProgram: webGLProgram,
+        uniformLocations: uniformLocations,
+        attributeLocations: attributeLocations,
+        gpgpu: gpgpu,
+        inShapeInfos: inShapeInfos,
+        outShapeInfo: outShapeInfo
+    };
+}
+exports.compileProgram = compileProgram;
+function validateBinaryAndProgram(shapeInfos, inputs) {
+    if (shapeInfos.length !== inputs.length) {
+        throw Error("Binary was compiled with " + shapeInfos.length + " inputs, but " +
+            ("was executed with " + inputs.length + " inputs"));
+    }
+    shapeInfos.forEach(function (s, i) {
+        var shapeA = s.logicalShape;
+        var texShapeA = s.texShape;
+        var shapeB = inputs[i].array.shape;
+        var texShapeB = inputs[i].texData.texShape;
+        if (!util.arraysEqual(shapeA, shapeB)) {
+            throw Error("Binary was compiled with different shapes than " +
+                ("the current args. Shapes " + shapeA + " and " + shapeB + " must match"));
+        }
+        if (!util.arraysEqual(texShapeA, texShapeB)) {
+            throw Error("Binary was compiled with different texture shapes than the" +
+                (" current args. Shape " + texShapeA + " and " + texShapeB + " must match"));
+        }
+    });
+}
+function runProgram(binary, inputs, output, customSetup) {
+    validateBinaryAndProgram(binary.inShapeInfos, inputs);
+    validateBinaryAndProgram([binary.outShapeInfo], [output]);
+    var outTex = output.texData.texture;
+    var outTexShape = output.texData.texShape;
+    var gpgpu = binary.gpgpu;
+    gpgpu.setOutputMatrixTexture(outTex, outTexShape[0], outTexShape[1]);
+    gpgpu.setProgram(binary.webGLProgram);
+    inputs.forEach(function (input, i) {
+        var tex = input.texData.texture;
+        var variableName = binary.program.variableNames[i];
+        var variableUniformLocation = binary.uniformLocations[variableName];
+        gpgpu.setInputMatrixTexture(tex, variableUniformLocation, i);
+    });
+    if (shouldUploadNaNUniform()) {
+        gpgpu.gl.uniform1f(binary.uniformLocations[NAN_UNIFORM_NAME], NaN);
+    }
+    if (customSetup != null) {
+        customSetup(gpgpu, binary.webGLProgram);
+    }
+    gpgpu.executeProgram(binary.attributeLocations);
+}
+exports.runProgram = runProgram;
+function makeShaderKey(program, inputs, output) {
+    var keyInputs = '';
+    inputs.concat(output).forEach(function (x) {
+        keyInputs += x.array.shape + "_" + x.texData.texShape;
+    });
+    var keyUserCode = program.userCode;
+    var keyBroadcast = (program.supportsBroadcasting === true).toString();
+    var key = program.constructor.name;
+    key += '_' + keyBroadcast + '_' + keyInputs + '_' + keyUserCode;
+    return key;
+}
+exports.makeShaderKey = makeShaderKey;
+
+},{"../../../environment":15,"../../../util":101,"./shader_compiler":82}],73:[function(require,module,exports){
+"use strict";
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __generator = (this && this.__generator) || function (thisArg, body) {
+    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
+    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
+    function verb(n) { return function (v) { return step([n, v]); }; }
+    function step(op) {
+        if (f) throw new TypeError("Generator is already executing.");
+        while (_) try {
+            if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
+            if (y = 0, t) op = [0, t.value];
+            switch (op[0]) {
+                case 0: case 1: t = op; break;
+                case 4: _.label++; return { value: op[1], done: false };
+                case 5: _.label++; y = op[1]; op = [0]; continue;
+                case 7: op = _.ops.pop(); _.trys.pop(); continue;
+                default:
+                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
+                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
+                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
+                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
+                    if (t[2]) _.ops.pop();
+                    _.trys.pop(); continue;
+            }
+            op = body.call(thisArg, _);
+        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
+        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
+    }
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../../environment");
+var tex_util = require("./tex_util");
+var webgl_util = require("./webgl_util");
+function getWebGLContextAttributes() {
+    return {
+        alpha: false,
+        antialias: false,
+        premultipliedAlpha: false,
+        preserveDrawingBuffer: false,
+        depth: false,
+        stencil: false,
+        failIfMajorPerformanceCaveat: true
+    };
+}
+exports.getWebGLContextAttributes = getWebGLContextAttributes;
+function createWebGLContext(canvas) {
+    var attributes = getWebGLContextAttributes();
+    var gl;
+    if (canvas != null) {
+        gl = webgl_util.createWebGLRenderingContextFromCanvas(canvas, attributes);
+    }
+    else {
+        gl = webgl_util.createWebGLRenderingContext(attributes);
+    }
+    webgl_util.callAndCheck(gl, function () { return gl.disable(gl.DEPTH_TEST); });
+    webgl_util.callAndCheck(gl, function () { return gl.disable(gl.STENCIL_TEST); });
+    webgl_util.callAndCheck(gl, function () { return gl.disable(gl.BLEND); });
+    webgl_util.callAndCheck(gl, function () { return gl.disable(gl.DITHER); });
+    webgl_util.callAndCheck(gl, function () { return gl.disable(gl.POLYGON_OFFSET_FILL); });
+    webgl_util.callAndCheck(gl, function () { return gl.disable(gl.SAMPLE_COVERAGE); });
+    webgl_util.callAndCheck(gl, function () { return gl.enable(gl.SCISSOR_TEST); });
+    webgl_util.callAndCheck(gl, function () { return gl.enable(gl.CULL_FACE); });
+    webgl_util.callAndCheck(gl, function () { return gl.cullFace(gl.BACK); });
+    return gl;
+}
+exports.createWebGLContext = createWebGLContext;
+function createVertexShader(gl) {
+    var vertexShaderSource = "\n    precision highp float;\n    attribute vec3 clipSpacePos;\n    attribute vec2 uv;\n    varying vec2 resultUV;\n\n    void main() {\n      gl_Position = vec4(clipSpacePos, 1);\n      resultUV = uv;\n    }";
+    return webgl_util.createVertexShader(gl, vertexShaderSource);
+}
+exports.createVertexShader = createVertexShader;
+function createVertexBuffer(gl) {
+    var vertexArray = new Float32Array([-1, 1, 0, 0, 1, -1, -1, 0, 0, 0, 1, 1, 0, 1, 1, 1, -1, 0, 1, 0]);
+    return webgl_util.createStaticVertexBuffer(gl, vertexArray);
+}
+exports.createVertexBuffer = createVertexBuffer;
+function createIndexBuffer(gl) {
+    var triangleVertexIndices = new Uint16Array([0, 1, 2, 2, 1, 3]);
+    return webgl_util.createStaticIndexBuffer(gl, triangleVertexIndices);
+}
+exports.createIndexBuffer = createIndexBuffer;
+function getTextureInternalFormat(gl, numChannels) {
+    if (!environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) {
+        return gl.RGBA;
+    }
+    if (environment_1.ENV.get('WEBGL_VERSION') === 2) {
+        if (numChannels === 4) {
+            return gl.RGBA32F;
+        }
+        return gl.R32F;
+    }
+    return gl.RGBA;
+}
+function getTextureFormat(gl, numChannels) {
+    if (!environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) {
+        return gl.RGBA;
+    }
+    if (environment_1.ENV.get('WEBGL_VERSION') === 2) {
+        if (numChannels === 4) {
+            return gl.RGBA;
+        }
+        return gl.RED;
+    }
+    return gl.RGBA;
+}
+function getTextureType(gl) {
+    if (!environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) {
+        return gl.UNSIGNED_BYTE;
+    }
+    return gl.FLOAT;
+}
+function createAndConfigureTexture(gl, width, height, numChannels) {
+    webgl_util.validateTextureSize(gl, width, height);
+    var texture = webgl_util.createTexture(gl);
+    var tex2d = gl.TEXTURE_2D;
+    var internalFormat = getTextureInternalFormat(gl, numChannels);
+    var format = getTextureFormat(gl, numChannels);
+    webgl_util.callAndCheck(gl, function () { return gl.bindTexture(tex2d, texture); });
+    webgl_util.callAndCheck(gl, function () { return gl.texParameteri(tex2d, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); });
+    webgl_util.callAndCheck(gl, function () { return gl.texParameteri(tex2d, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); });
+    webgl_util.callAndCheck(gl, function () { return gl.texParameteri(tex2d, gl.TEXTURE_MIN_FILTER, gl.NEAREST); });
+    webgl_util.callAndCheck(gl, function () { return gl.texParameteri(tex2d, gl.TEXTURE_MAG_FILTER, gl.NEAREST); });
+    webgl_util.callAndCheck(gl, function () { return gl.texImage2D(tex2d, 0, internalFormat, width, height, 0, format, getTextureType(gl), null); });
+    webgl_util.callAndCheck(gl, function () { return gl.bindTexture(gl.TEXTURE_2D, null); });
+    return texture;
+}
+function createMatrixTexture(gl, rows, columns) {
+    var _a = tex_util.getUnpackedMatrixTextureShapeWidthHeight(rows, columns), width = _a[0], height = _a[1];
+    var numChannels = 1;
+    return createAndConfigureTexture(gl, width, height, numChannels);
+}
+exports.createMatrixTexture = createMatrixTexture;
+function createColorMatrixTexture(gl, rows, columns) {
+    var _a = tex_util.getColorMatrixTextureShapeWidthHeight(rows, columns), width = _a[0], height = _a[1];
+    var numChannels = 4;
+    return createAndConfigureTexture(gl, width, height, numChannels);
+}
+exports.createColorMatrixTexture = createColorMatrixTexture;
+function createPackedMatrixTexture(gl, rows, columns) {
+    var _a = tex_util.getPackedMatrixTextureShapeWidthHeight(rows, columns), width = _a[0], height = _a[1];
+    var numChannels = 4;
+    return createAndConfigureTexture(gl, width, height, numChannels);
+}
+exports.createPackedMatrixTexture = createPackedMatrixTexture;
+function bindVertexProgramAttributeStreams(gl, program, vertexBuffer, attribLocations) {
+    var posOffset = 0;
+    var uvOffset = 3 * 4;
+    var stride = (3 * 4) + (2 * 4);
+    webgl_util.callAndCheck(gl, function () { return gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); });
+    webgl_util.bindVertexBufferToProgramAttribute(gl, program, 'clipSpacePos', vertexBuffer, 3, stride, posOffset, attribLocations);
+    webgl_util.bindVertexBufferToProgramAttribute(gl, program, 'uv', vertexBuffer, 2, stride, uvOffset, attribLocations);
+}
+exports.bindVertexProgramAttributeStreams = bindVertexProgramAttributeStreams;
+function uploadPixelDataToTexture(gl, texture, pixels) {
+    webgl_util.callAndCheck(gl, function () { return gl.bindTexture(gl.TEXTURE_2D, texture); });
+    webgl_util.callAndCheck(gl, function () { return gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, pixels); });
+    webgl_util.callAndCheck(gl, function () { return gl.bindTexture(gl.TEXTURE_2D, null); });
+}
+exports.uploadPixelDataToTexture = uploadPixelDataToTexture;
+function uploadDataToTexture(gl, texture, width, height, data, numChannels) {
+    var textureFormat = getTextureFormat(gl, numChannels);
+    webgl_util.validateTextureSize(gl, width, height);
+    webgl_util.callAndCheck(gl, function () { return gl.bindTexture(gl.TEXTURE_2D, texture); });
+    webgl_util.callAndCheck(gl, function () { return gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, width, height, textureFormat, getTextureType(gl), data); });
+    webgl_util.callAndCheck(gl, function () { return gl.bindTexture(gl.TEXTURE_2D, null); });
+}
+function uploadMatrixToTexture(gl, texture, rows, columns, matrix, numChannels) {
+    var _a = tex_util.getUnpackedMatrixTextureShapeWidthHeight(rows, columns), w = _a[0], h = _a[1];
+    var unpackedArray;
+    if (environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) {
+        var channelsPerTexture = numChannels === 1 ? webgl_util.getChannelsPerTexture() : numChannels;
+        if (channelsPerTexture === 1) {
+            unpackedArray = matrix;
+        }
+        else {
+            unpackedArray =
+                new Float32Array(tex_util.getUnpackedArraySizeFromMatrixSize(matrix.length, channelsPerTexture));
+            tex_util.encodeMatrixToUnpackedArray(matrix, unpackedArray, channelsPerTexture);
+        }
+    }
+    else {
+        unpackedArray = tex_util.encodeFloatArray(matrix);
+    }
+    uploadDataToTexture(gl, texture, w, h, unpackedArray, numChannels);
+}
+exports.uploadMatrixToTexture = uploadMatrixToTexture;
+function uploadMatrixToPackedTexture(gl, texture, rows, columns, matrix) {
+    var _a = tex_util.getPackedMatrixTextureShapeWidthHeight(rows, columns), w = _a[0], h = _a[1];
+    var packedRGBA = new Float32Array(tex_util.getPackedRGBAArraySizeFromMatrixShape(rows, columns));
+    tex_util.encodeMatrixToPackedRGBA(matrix, rows, columns, packedRGBA);
+    var numChannels = 4;
+    uploadDataToTexture(gl, texture, w, h, packedRGBA, numChannels);
+}
+exports.uploadMatrixToPackedTexture = uploadMatrixToPackedTexture;
+function getDownloadTargetArrayBuffer(rows, columns, channelsPerTexture) {
+    var isFloatTexture = environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED');
+    var downloadTarget;
+    if (isFloatTexture) {
+        downloadTarget =
+            new Float32Array(tex_util.getUnpackedArraySizeFromMatrixSize(rows * columns, channelsPerTexture));
+    }
+    else {
+        downloadTarget = new Uint8Array(rows * columns * channelsPerTexture);
+    }
+    return downloadTarget;
+}
+function decodeDownloadTargetArrayBuffer(downloadTarget, rows, columns, channelsPerPixel) {
+    var isFloatTexture = environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED');
+    if (isFloatTexture) {
+        var matrix = new Float32Array(rows * columns);
+        tex_util.decodeMatrixFromUnpackedArray(downloadTarget, matrix, channelsPerPixel);
+        return matrix;
+    }
+    else {
+        return tex_util.decodeToFloatArray(downloadTarget);
+    }
+}
+function downloadMatrixFromOutputTextureAsync(gl, getBufferSubDataAsyncExtension, rows, columns) {
+    return __awaiter(this, void 0, void 0, function () {
+        var gl2, channelsPerPixel, downloadTarget, bufferSizeBytes, buffer;
+        return __generator(this, function (_a) {
+            switch (_a.label) {
+                case 0:
+                    gl2 = gl;
+                    channelsPerPixel = 4;
+                    downloadTarget = getDownloadTargetArrayBuffer(rows, columns, channelsPerPixel);
+                    bufferSizeBytes = downloadTarget instanceof Float32Array ?
+                        downloadTarget.length * 4 :
+                        downloadTarget;
+                    buffer = gl.createBuffer();
+                    webgl_util.callAndCheck(gl, function () { return gl.bindBuffer(gl2.PIXEL_PACK_BUFFER, buffer); });
+                    webgl_util.callAndCheck(gl, function () { return gl.bufferData(gl2.PIXEL_PACK_BUFFER, bufferSizeBytes, gl.STATIC_DRAW); });
+                    webgl_util.callAndCheck(gl, function () {
+                        return gl2.readPixels(0, 0, columns, rows, gl.RGBA, getTextureType(gl), 0);
+                    });
+                    return [4, getBufferSubDataAsyncExtension.getBufferSubDataAsync(gl2.PIXEL_PACK_BUFFER, 0, downloadTarget)];
+                case 1:
+                    _a.sent();
+                    return [2, decodeDownloadTargetArrayBuffer(downloadTarget, rows, columns, channelsPerPixel)];
+            }
+        });
+    });
+}
+exports.downloadMatrixFromOutputTextureAsync = downloadMatrixFromOutputTextureAsync;
+function downloadMatrixFromOutputTexture(gl, rows, columns) {
+    var _a = tex_util.getUnpackedMatrixTextureShapeWidthHeight(rows, columns), w = _a[0], h = _a[1];
+    var channelsPerPixel = 4;
+    var downloadTarget = getDownloadTargetArrayBuffer(rows, columns, channelsPerPixel);
+    webgl_util.callAndCheck(gl, function () { return gl.readPixels(0, 0, w, h, gl.RGBA, getTextureType(gl), downloadTarget); });
+    return decodeDownloadTargetArrayBuffer(downloadTarget, rows, columns, channelsPerPixel);
+}
+exports.downloadMatrixFromOutputTexture = downloadMatrixFromOutputTexture;
+function downloadMatrixFromRGBAColorTexture(gl, rows, columns, channels) {
+    var size = rows * columns * 4;
+    var downloadTarget = new Uint8Array(size);
+    webgl_util.callAndCheck(gl, function () { return gl.readPixels(0, 0, columns, rows, gl.RGBA, gl.UNSIGNED_BYTE, downloadTarget); });
+    var packedRGBA = new Float32Array(size);
+    for (var i = 0; i < downloadTarget.length; i++) {
+        packedRGBA[i] = downloadTarget[i];
+    }
+    var matrix = new Float32Array(rows * columns * channels);
+    tex_util.decodeMatrixFromUnpackedColorRGBAArray(packedRGBA, matrix, channels);
+    return matrix;
+}
+exports.downloadMatrixFromRGBAColorTexture = downloadMatrixFromRGBAColorTexture;
+function downloadMatrixFromPackedOutputTexture(gl, rows, columns) {
+    var _a = tex_util.getPackedMatrixTextureShapeWidthHeight(rows, columns), w = _a[0], h = _a[1];
+    var packedRGBA = new Float32Array(tex_util.getPackedRGBAArraySizeFromMatrixShape(rows, columns));
+    webgl_util.callAndCheck(gl, function () { return gl.readPixels(0, 0, w, h, gl.RGBA, getTextureType(gl), packedRGBA); });
+    var matrix = new Float32Array(rows * columns);
+    return tex_util.decodeMatrixFromPackedRGBA(packedRGBA, rows, columns, matrix);
+}
+exports.downloadMatrixFromPackedOutputTexture = downloadMatrixFromPackedOutputTexture;
+
+},{"../../../environment":15,"./tex_util":84,"./webgl_util":89}],74:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var MaxPool2DBackpropProgram = (function () {
+    function MaxPool2DBackpropProgram(convInfo) {
+        this.variableNames = ['dy', 'maxPos'];
+        this.outputShape = convInfo.inShape;
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var padTop = filterHeight - 1 - convInfo.padInfo.top;
+        var padLeft = filterWidth - 1 - convInfo.padInfo.left;
+        var lastIndex = filterHeight * filterWidth - 1;
+        this.userCode = "\n      const ivec2 pads = ivec2(" + padTop + ", " + padLeft + ");\n\n      void main() {\n        ivec4 coords = getOutputCoords();\n        int b = coords[0];\n        int d = coords[3];\n\n        ivec2 dyRCCorner = coords.yz - pads;\n        int dyRCorner = dyRCCorner.x;\n        int dyCCorner = dyRCCorner.y;\n\n        // Convolve dy(?, ?, d) with pos mask(:, :, d) to get dx(xR, xC, d).\n        // ? = to be determined. : = across all values in that axis.\n        float dotProd = 0.0;\n        for (int wR = 0; wR < " + filterHeight + "; wR++) {\n          float dyR = float(dyRCorner + wR) / " + strideHeight + ".0;\n\n          if (dyR < 0.0 || dyR >= " + convInfo.outHeight + ".0 || fract(dyR) > 0.0) {\n            continue;\n          }\n          int idyR = int(dyR);\n\n          for (int wC = 0; wC < " + filterWidth + "; wC++) {\n            float dyC = float(dyCCorner + wC) / " + strideWidth + ".0;\n\n            if (dyC < 0.0 || dyC >= " + convInfo.outWidth + ".0 ||\n                fract(dyC) > 0.0) {\n              continue;\n            }\n            int idyC = int(dyC);\n\n            float dyValue = getDy(b, idyR, idyC, d);\n            int maxPosValue = " + lastIndex + " - int(getMaxPos(b, idyR, idyC, d));\n\n            // Get the current value, check it against the value from the\n            // position matrix.\n            int curPosValue = wR * " + filterWidth + " + wC;\n            float mask = float(maxPosValue == curPosValue ? 1.0 : 0.0);\n\n            dotProd += dyValue * mask;\n          }\n        }\n        setOutput(dotProd);\n      }\n    ";
+    }
+    return MaxPool2DBackpropProgram;
+}());
+exports.MaxPool2DBackpropProgram = MaxPool2DBackpropProgram;
+
+},{}],75:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var matmul_1 = require("../types/matmul");
+var MatMulProgram = (function () {
+    function MatMulProgram(aShape, bShape, aOrient, bOrient) {
+        if (aOrient === void 0) { aOrient = matmul_1.MatrixOrientation.REGULAR; }
+        if (bOrient === void 0) { bOrient = matmul_1.MatrixOrientation.REGULAR; }
+        this.variableNames = ['matrixA', 'matrixB'];
+        var outerShapeA = (aOrient === matmul_1.MatrixOrientation.REGULAR) ? aShape[0] : aShape[1];
+        var outerShapeB = (bOrient === matmul_1.MatrixOrientation.REGULAR) ? bShape[1] : bShape[0];
+        this.outputShape = [outerShapeA, outerShapeB];
+        var sharedDim = (aOrient === matmul_1.MatrixOrientation.REGULAR ? aShape[1] : aShape[0]);
+        var aSnippetFromOffset = function (vec4Offset, indexVar) {
+            return (aOrient === matmul_1.MatrixOrientation.REGULAR) ?
+                "aRow, " + indexVar + " + " + vec4Offset :
+                indexVar + " + " + vec4Offset + ", aRow";
+        };
+        var bSnippetFromOffset = function (vec4Offset, indexVar) {
+            return (bOrient === matmul_1.MatrixOrientation.REGULAR) ?
+                indexVar + " + " + vec4Offset + ", bCol" :
+                "bCol, " + indexVar + " + " + vec4Offset;
+        };
+        var sharedDimNearestVec4 = Math.floor(sharedDim / 4) * 4;
+        var sharedDimVec4Remainder = sharedDim % 4;
+        this.userCode = " float dotARowBCol(int aRow, int bCol) {\n      float result = 0.0;\n      for (int i = 0; i < " + sharedDimNearestVec4 + "; i += 4) {\n        vec4 a = vec4(\n          getMatrixA(" + aSnippetFromOffset(0, 'i') + "),\n          getMatrixA(" + aSnippetFromOffset(1, 'i') + "),\n          getMatrixA(" + aSnippetFromOffset(2, 'i') + "),\n          getMatrixA(" + aSnippetFromOffset(3, 'i') + ")\n        );\n        vec4 b = vec4(\n          getMatrixB(" + bSnippetFromOffset(0, 'i') + "),\n          getMatrixB(" + bSnippetFromOffset(1, 'i') + "),\n          getMatrixB(" + bSnippetFromOffset(2, 'i') + "),\n          getMatrixB(" + bSnippetFromOffset(3, 'i') + ")\n        );\n\n        result += dot(a, b);\n      }\n\n      if (" + (sharedDimVec4Remainder === 1) + ") {\n        result += getMatrixA(" + aSnippetFromOffset(0, sharedDimNearestVec4) + ") *\n          getMatrixB(" + bSnippetFromOffset(0, sharedDimNearestVec4) + ");\n      } else if (" + (sharedDimVec4Remainder === 2) + ") {\n        vec2 a = vec2(\n          getMatrixA(" + aSnippetFromOffset(0, sharedDimNearestVec4) + "),\n          getMatrixA(" + aSnippetFromOffset(1, sharedDimNearestVec4) + ")\n        );\n        vec2 b = vec2(\n          getMatrixB(" + bSnippetFromOffset(0, sharedDimNearestVec4) + "),\n          getMatrixB(" + bSnippetFromOffset(1, sharedDimNearestVec4) + ")\n        );\n        result += dot(a, b);\n      } else if (" + (sharedDimVec4Remainder === 3) + ") {\n        vec3 a = vec3(\n          getMatrixA(" + aSnippetFromOffset(0, sharedDimNearestVec4) + "),\n          getMatrixA(" + aSnippetFromOffset(1, sharedDimNearestVec4) + "),\n          getMatrixA(" + aSnippetFromOffset(2, sharedDimNearestVec4) + ")\n        );\n        vec3 b = vec3(\n          getMatrixB(" + bSnippetFromOffset(0, sharedDimNearestVec4) + "),\n          getMatrixB(" + bSnippetFromOffset(1, sharedDimNearestVec4) + "),\n          getMatrixB(" + bSnippetFromOffset(2, sharedDimNearestVec4) + ")\n        );\n        result += dot(a, b);\n      }\n\n      return result;\n    }\n\n    void main() {\n      ivec2 resRC = getOutputCoords();\n      setOutput(dotARowBCol(resRC.x, resRC.y));\n    }\n    ";
+    }
+    return MatMulProgram;
+}());
+exports.MatMulProgram = MatMulProgram;
+
+},{"../types/matmul":61}],76:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var MultinomialProgram = (function () {
+    function MultinomialProgram(batchSize, numOutcomes, numSamples) {
+        this.variableNames = ['probs'];
+        this.outputShape = [batchSize, numSamples];
+        this.userCode = "\n      uniform float seed;\n\n      void main() {\n        ivec2 coords = getOutputCoords();\n        int batch = coords[0];\n\n        float r = random(seed);\n        float cdf = 0.0;\n\n        for (int i = 0; i < " + (numOutcomes - 1) + "; i++) {\n          cdf += getProbs(batch, i);\n\n          if (r < cdf) {\n            setOutput(float(i));\n            return;\n          }\n        }\n\n        // If no other event happened, last event happened.\n        setOutput(float(" + (numOutcomes - 1) + "));\n      }\n    ";
+    }
+    MultinomialProgram.prototype.getCustomSetupFunc = function (seed) {
+        var _this = this;
+        return function (gpgpu, webGLProgram) {
+            if (_this.seedLoc == null) {
+                _this.seedLoc = gpgpu.getUniformLocation(webGLProgram, 'seed');
+            }
+            gpgpu.gl.uniform1f(_this.seedLoc, seed);
+        };
+    };
+    return MultinomialProgram;
+}());
+exports.MultinomialProgram = MultinomialProgram;
+
+},{}],77:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var OneHotProgram = (function () {
+    function OneHotProgram(numIndices, depth, onValue, offValue) {
+        this.variableNames = ['indices'];
+        this.outputShape = [numIndices, depth];
+        this.userCode = "\n      void main() {\n        ivec2 coords = getOutputCoords();\n        int index = round(getIndices(coords.x));\n        setOutput(mix(float(" + offValue + "), float(" + onValue + "),\n                      float(index == coords.y)));\n      }\n    ";
+    }
+    OneHotProgram.prototype.getCustomSetupFunc = function (seed) {
+        var _this = this;
+        return function (gpgpu, webGLProgram) {
+            if (_this.seedLoc == null) {
+                _this.seedLoc = gpgpu.getUniformLocation(webGLProgram, 'seed');
+            }
+            gpgpu.gl.uniform1f(_this.seedLoc, seed);
+        };
+    };
+    return OneHotProgram;
+}());
+exports.OneHotProgram = OneHotProgram;
+
+},{}],78:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var Pool2DProgram = (function () {
+    function Pool2DProgram(convInfo, poolType, computePositions) {
+        this.variableNames = ['x'];
+        if (poolType === 'avg' && computePositions) {
+            throw new Error('Cannot compute positions for average pool.');
+        }
+        var filterHeight = convInfo.filterHeight;
+        var filterWidth = convInfo.filterWidth;
+        var strideHeight = convInfo.strideHeight;
+        var strideWidth = convInfo.strideWidth;
+        var padTop = convInfo.padInfo.top;
+        var padLeft = convInfo.padInfo.left;
+        this.outputShape = convInfo.outShape;
+        var isAvgPool = poolType === 'avg';
+        var initializationValue = '0.0';
+        if (!isAvgPool) {
+            if (poolType === 'min') {
+                initializationValue = '1.0 / 0.0';
+            }
+            else {
+                initializationValue = '-1.0 / 0.0';
+            }
+        }
+        if (computePositions) {
+            var compareOp_1 = poolType === 'min' ? '<=' : '>=';
+            this.userCode = "\n        const ivec2 strides = ivec2(" + strideHeight + ", " + strideWidth + ");\n        const ivec2 pads = ivec2(" + padTop + ", " + padLeft + ");\n\n        void main() {\n          ivec4 coords = getOutputCoords();\n          int batch = coords[0];\n          int d = coords[3];\n\n          ivec2 xRCCorner = coords.yz * strides - pads;\n          int xRCorner = xRCCorner.x;\n          int xCCorner = xRCCorner.y;\n\n          // max/min x(?, ?, d) to get y(yR, yC, d).\n          // ? = to be determined\n          float minMaxValue = 0.0;\n          float minMaxValueFound = 0.0;\n          int minMaxPosition = 0;\n          float avgValue = 0.0;\n\n          for (int wR = 0; wR < " + filterHeight + "; wR++) {\n            int xR = xRCorner + wR;\n\n            if (xR < 0 || xR >= " + convInfo.inHeight + ") {\n              continue;\n            }\n\n            for (int wC = 0; wC < " + filterWidth + "; wC++) {\n              int xC = xCCorner + wC;\n\n              if (xC < 0 || xC >= " + convInfo.inWidth + ") {\n                continue;\n              }\n\n              float value = getX(batch, xR, xC, d);\n\n              if (isNaN(value)) {\n                setOutput(value);\n                return;\n              }\n\n              // If a min / max value has already been found, use it. If not,\n              // use the current value.\n              float currMinMaxValue = mix(\n                  value, minMaxValue, minMaxValueFound);\n              if (value " + compareOp_1 + " currMinMaxValue) {\n                minMaxValue = value;\n                minMaxValueFound = 1.0;\n                minMaxPosition = wR * " + filterWidth + " + wC;\n              }\n            }\n          }\n          setOutput(float(minMaxPosition));\n        }\n      ";
+            return;
+        }
+        var compareOp = poolType === 'min' ? 'min' : 'max';
+        var returnValue = poolType + "(" + poolType + "(" + poolType + "(" +
+            'minMaxValue[0], minMaxValue[1]), minMaxValue[2]), minMaxValue[3])';
+        if (poolType === 'avg') {
+            returnValue = "avgValue / " + filterHeight * filterWidth + ".0";
+        }
+        var filterWidthNearestVec4 = Math.floor(filterWidth / 4) * 4;
+        var filterWidthVec4Remainder = filterWidth % 4;
+        var updateSnippet = "\n      if (hasNaN(values)) {\n        setOutput(getNaN(values));\n        return;\n      }\n      if (" + isAvgPool + ") {\n        avgValue += dot(values, ones);\n      } else {\n        minMaxValue = " + compareOp + "(values, minMaxValue);\n      }\n    ";
+        this.userCode = "\n      const ivec2 strides = ivec2(" + strideHeight + ", " + strideWidth + ");\n      const ivec2 pads = ivec2(" + padTop + ", " + padLeft + ");\n      const float initializationValue = " + initializationValue + ";\n      const vec4 ones = vec4(1.0, 1.0, 1.0, 1.0);\n\n      float getValue(int batch, int xR, int xC, int d) {\n        if (xC < 0 || xC >= " + convInfo.inWidth + ") {\n          return initializationValue;\n        }\n        return getX(batch, xR, xC, d);\n      }\n\n      void main() {\n        ivec4 coords = getOutputCoords();\n        int batch = coords[0];\n        int d = coords[3];\n\n        ivec2 xRCCorner = coords.yz * strides - pads;\n        int xRCorner = xRCCorner.x;\n        int xCCorner = xRCCorner.y;\n\n        // max/min x(?, ?, d) to get y(yR, yC, d).\n        // ? = to be determined\n        vec4 minMaxValue = vec4(" + initializationValue + ");\n        float avgValue = 0.0;\n\n        for (int wR = 0; wR < " + filterHeight + "; wR++) {\n          int xR = xRCorner + wR;\n\n          if (xR < 0 || xR >= " + convInfo.inHeight + ") {\n            continue;\n          }\n\n          for (int wC = 0; wC < " + filterWidthNearestVec4 + "; wC += 4) {\n            int xC = xCCorner + wC;\n\n            vec4 values = vec4(\n              getValue(batch, xR, xC, d),\n              getValue(batch, xR, xC + 1, d),\n              getValue(batch, xR, xC + 2, d),\n              getValue(batch, xR, xC + 3, d)\n            );\n\n            " + updateSnippet + "\n          }\n\n          int xC = xCCorner + " + filterWidthNearestVec4 + ";\n          if (" + (filterWidthVec4Remainder === 1) + ") {\n            vec4 values = vec4(\n              getValue(batch, xR, xC, d),\n              initializationValue,\n              initializationValue,\n              initializationValue\n            );\n            " + updateSnippet + "\n          } else if (" + (filterWidthVec4Remainder === 2) + ") {\n            vec4 values = vec4(\n              getValue(batch, xR, xC, d),\n              getValue(batch, xR, xC + 1, d),\n              initializationValue,\n              initializationValue\n            );\n\n            " + updateSnippet + "\n          } else if (" + (filterWidthVec4Remainder === 3) + ") {\n            vec4 values = vec4(\n              getValue(batch, xR, xC, d),\n              getValue(batch, xR, xC + 1, d),\n              getValue(batch, xR, xC + 2, d),\n              initializationValue\n            );\n\n            " + updateSnippet + "\n          }\n        }\n        setOutput(" + returnValue + ");\n      }\n    ";
+    }
+    return Pool2DProgram;
+}());
+exports.Pool2DProgram = Pool2DProgram;
+
+},{}],79:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ReduceProgram = (function () {
+    function ReduceProgram(reduceInfo, reduceType) {
+        this.variableNames = ['x'];
+        var windowSize = reduceInfo.windowSize;
+        var batchSize = reduceInfo.batchSize;
+        var inSize = reduceInfo.inSize;
+        var outSize = Math.ceil(inSize / windowSize);
+        this.outputShape = [batchSize, outSize];
+        var isReduceSum = reduceType === 'sum';
+        var initializationValue = '0.0';
+        if (!isReduceSum) {
+            if (reduceType === 'min') {
+                initializationValue = '1.0 / 0.0';
+            }
+            else {
+                initializationValue = '-1.0 / 0.0';
+            }
+        }
+        var compareOp = reduceType === 'min' ? 'min' : 'max';
+        var returnValue = reduceType + "(" + reduceType + "(" + reduceType + "(" +
+            'minMaxValue[0], minMaxValue[1]), minMaxValue[2]), minMaxValue[3])';
+        if (reduceType === 'sum') {
+            returnValue = "sumValue";
+        }
+        var windowSizeNearestVec4 = Math.floor(windowSize / 4) * 4;
+        var windowSizeVec4Remainder = windowSize % 4;
+        var updateSnippet = "\n      if (" + isReduceSum + ") {\n        sumValue += dot(values, ones);\n      } else {\n        if (hasNaN(values)) {\n          setOutput(getNaN(values));\n          return;\n        }\n        minMaxValue = " + compareOp + "(values, minMaxValue);\n      }\n    ";
+        var checkOutOfBounds = '';
+        if (inSize % windowSize > 0) {
+            checkOutOfBounds = "\n        if (inIdx < 0 || inIdx >= " + inSize + ") {\n          return initializationValue;\n        }\n      ";
+        }
+        this.userCode = "\n      const float initializationValue = " + initializationValue + ";\n      const vec4 ones = vec4(1.0, 1.0, 1.0, 1.0);\n\n      float getValue(int batch, int inIdx) {\n        " + checkOutOfBounds + "\n        return getX(batch, inIdx);\n      }\n\n      void main() {\n        ivec2 coords = getOutputCoords();\n        int batch = coords[0];\n        int outIdx = coords[1];\n        int inOffset = outIdx * " + windowSize + ";\n\n        vec4 minMaxValue = vec4(" + initializationValue + ");\n        float sumValue = 0.0;\n\n        for (int i = 0; i < " + windowSizeNearestVec4 + "; i += 4) {\n          int inIdx = inOffset + i;\n          vec4 values = vec4(\n            getValue(batch, inIdx),\n            getValue(batch, inIdx + 1),\n            getValue(batch, inIdx + 2),\n            getValue(batch, inIdx + 3)\n          );\n\n          " + updateSnippet + "\n        }\n\n        int inIdx = inOffset + " + windowSizeNearestVec4 + ";\n        if (" + (windowSizeVec4Remainder === 1) + ") {\n          vec4 values = vec4(\n            getValue(batch, inIdx),\n            initializationValue,\n            initializationValue,\n            initializationValue\n          );\n          " + updateSnippet + "\n        } else if (" + (windowSizeVec4Remainder === 2) + ") {\n          vec4 values = vec4(\n            getValue(batch, inIdx),\n            getValue(batch, inIdx + 1),\n            initializationValue,\n            initializationValue\n          );\n          " + updateSnippet + "\n        } else if (" + (windowSizeVec4Remainder === 3) + ") {\n          vec4 values = vec4(\n            getValue(batch, inIdx),\n            getValue(batch, inIdx + 1),\n            getValue(batch, inIdx + 2),\n            initializationValue\n          );\n          " + updateSnippet + "\n        }\n        setOutput(" + returnValue + ");\n      }\n    ";
+    }
+    return ReduceProgram;
+}());
+exports.ReduceProgram = ReduceProgram;
+
+},{}],80:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var webgl_util = require("./webgl_util");
+function getRenderRGBShader(gpgpu, destinationWidth) {
+    var fragmentShaderSource = "\n    precision highp float;\n    uniform sampler2D source;\n    varying vec2 resultUV;\n\n    const float destinationWidth = " + destinationWidth + ".0;\n    const float a = 1.0;\n\n    void main() {\n      float xr = floor(resultUV.s * destinationWidth) * 3.0;\n      vec3 x = xr + vec3(0, 1, 2);\n\n      float sourceWidth = destinationWidth * 3.0;\n      vec3 u = (x + 0.5) / sourceWidth;\n      float v = 1.0 - resultUV.t;\n\n      float r = texture2D(source, vec2(u[0], v)).r;\n      float g = texture2D(source, vec2(u[1], v)).r;\n      float b = texture2D(source, vec2(u[2], v)).r;\n\n      gl_FragColor = vec4(r, g, b, a);\n    }";
+    return gpgpu.createProgram(fragmentShaderSource);
+}
+exports.getRenderRGBShader = getRenderRGBShader;
+function renderToCanvas(gpgpu, renderShader, sourceTex) {
+    webgl_util.bindCanvasToFramebuffer(gpgpu.gl);
+    renderToFramebuffer(gpgpu, renderShader, sourceTex);
+}
+exports.renderToCanvas = renderToCanvas;
+function renderToFramebuffer(gpgpu, renderShader, sourceTex) {
+    gpgpu.setProgram(renderShader);
+    var sourceSamplerLocation = webgl_util.getProgramUniformLocationOrThrow(gpgpu.gl, renderShader, 'source');
+    gpgpu.setInputMatrixTexture(sourceTex, sourceSamplerLocation, 0);
+    gpgpu.executeProgram();
+}
+exports.renderToFramebuffer = renderToFramebuffer;
+
+},{"./webgl_util":89}],81:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var ResizeBilinear3DProgram = (function () {
+    function ResizeBilinear3DProgram(inputShape, outputDimensionsRowCol, alignCorners) {
+        this.variableNames = ['A'];
+        this.outputShape = [];
+        var depth = inputShape[2];
+        this.outputShape =
+            [outputDimensionsRowCol[0], outputDimensionsRowCol[1], depth];
+        var effectiveInputShape = alignCorners ?
+            [inputShape[0] - 1, inputShape[1] - 1, depth] :
+            inputShape;
+        var effectiveOutputShape = alignCorners ?
+            [this.outputShape[0] - 1, this.outputShape[1] - 1, depth] :
+            this.outputShape;
+        this.userCode = "\n      const vec2 effectiveInputOverOutputRatioRC = vec2(\n          " + effectiveInputShape[0] / effectiveOutputShape[0] + ",\n          " + effectiveInputShape[1] / effectiveOutputShape[1] + ");\n      const vec2 inputShapeRC = vec2(" + inputShape[0] + ".0, " + inputShape[1] + ".0);\n\n      void main() {\n        ivec3 coords = getOutputCoords();\n        ivec2 yRC = coords.xy;\n        int d = coords.z;\n\n        // Fractional source index.\n        vec2 sourceFracIndexRC = vec2(yRC) * effectiveInputOverOutputRatioRC;\n\n        // Compute the four integer indices.\n        ivec2 sourceFloorRC = ivec2(sourceFracIndexRC);\n        ivec2 sourceCeilRC = ivec2(\n          min(inputShapeRC - 1.0, ceil(sourceFracIndexRC)));\n\n        float topLeft = getA(sourceFloorRC.x, sourceFloorRC.y, d);\n        float bottomLeft = getA(sourceCeilRC.x, sourceFloorRC.y, d);\n        float topRight = getA(sourceFloorRC.x, sourceCeilRC.y, d);\n        float bottomRight = getA(sourceCeilRC.x, sourceCeilRC.y, d);\n\n        vec2 fracRC = sourceFracIndexRC - vec2(sourceFloorRC);\n\n        float top = topLeft + (topRight - topLeft) * fracRC.y;\n        float bottom = bottomLeft + (bottomRight - bottomLeft) * fracRC.y;\n        float newValue = top + (bottom - top) * fracRC.x;\n\n        setOutput(newValue);\n      }\n    ";
+    }
+    return ResizeBilinear3DProgram;
+}());
+exports.ResizeBilinear3DProgram = ResizeBilinear3DProgram;
+
+},{}],82:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../../../environment");
+var util = require("../../../util");
+var broadcast_util = require("../../broadcast_util");
+var tex_util = require("./tex_util");
+var tex_util_1 = require("./tex_util");
+function makeShader(inputsInfo, outputShape, userCode, broadcast) {
+    var sampleSnippet = getSampleSnippet();
+    var setOutputSnippet = getSetOutputSnippet();
+    var inputPrefixSnippet = inputsInfo.map(function (x) { return "uniform sampler2D " + x.name + ";"; }).join('\n');
+    var inputSamplingSnippet = inputsInfo.map(function (x) { return getInputSamplingSnippet(x, outputShape, broadcast); })
+        .join('\n');
+    var outTexShape = outputShape.texShape;
+    var outputSamplingSnippet = getOutputSamplingSnippet(outputShape.logicalShape, outTexShape);
+    var source = [
+        SHADER_PREFIX, sampleSnippet, setOutputSnippet, inputPrefixSnippet,
+        outputSamplingSnippet, inputSamplingSnippet, userCode
+    ].join('\n');
+    return source;
+}
+exports.makeShader = makeShader;
+function getSampleSnippet() {
+    return environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED') ?
+        FLOAT_TEXTURE_SAMPLE_SNIPPET :
+        UNSIGNED_BYTE_TEXTURE_SAMPLE_SNIPPET;
+}
+function getSetOutputSnippet() {
+    return environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED') ?
+        FLOAT_TEXTURE_SETOUTPUT_SNIPPET :
+        UNSIGNED_BYTE_TEXTURE_SETOUTPUT_SNIPPET;
+}
+function getSamplerFromInInfo(inInfo) {
+    var shape = inInfo.shapeInfo.logicalShape;
+    switch (shape.length) {
+        case 0:
+            return getSamplerScalar(inInfo);
+        case 1:
+            return getSampler1D(inInfo);
+        case 2:
+            return getSampler2D(inInfo);
+        case 3:
+            return getSampler3D(inInfo);
+        case 4:
+            return getSampler4D(inInfo);
+        default:
+            throw new Error(shape.length + "-D input sampling" +
+                " is not yet supported");
+    }
+}
+function getInputSamplingSnippet(inInfo, outShapeInfo, broadcast) {
+    var res = getSamplerFlat(inInfo);
+    res += getSamplerFromInInfo(inInfo);
+    if (broadcast ||
+        util.arraysEqual(inInfo.shapeInfo.logicalShape, outShapeInfo.logicalShape)) {
+        res += getSamplerAtOutputCoords(inInfo, outShapeInfo, broadcast);
+    }
+    return res;
+}
+function getOutputSamplingSnippet(outShape, outTexShape) {
+    switch (outShape.length) {
+        case 0:
+            return getOutputScalarCoords();
+        case 1:
+            return getOutput1DCoords(outShape, outTexShape);
+        case 2:
+            return getOutput2DCoords(outShape, outTexShape);
+        case 3:
+            return getOutput3DCoords(outShape, outTexShape);
+        case 4:
+            return getOutput4DCoords(outShape, outTexShape);
+        default:
+            throw new Error(outShape.length + "-D output sampling is not yet supported");
+    }
+}
+var SAMPLE_1D_SNIPPET = "\nvec2 UVfrom1D(int texNumR, int texNumC, int index) {\n  int texR = index / texNumC;\n  int texC = index - texR * texNumC;\n  return (vec2(texC, texR) + halfCR) / vec2(texNumC, texNumR);\n}\n";
+var SAMPLE_2D_SNIPPET = "\nvec2 UVfrom2D(int texNumR, int texNumC, int numC, int row, int col) {\n  int index = row * numC + col;\n  int texR = index / texNumC;\n  int texC = index - texR * texNumC;\n  return (vec2(texC, texR) + halfCR) / vec2(texNumC, texNumR);\n}\n";
+var SAMPLE_3D_SNIPPET = "\nvec2 UVfrom3D(int texNumR, int texNumC, int stride0,\n    int stride1, int row, int col, int depth) {\n  // Explicitly use integer operations as dot() only works on floats.\n  int index = row * stride0 + col * stride1 + depth;\n  int texR = index / texNumC;\n  int texC = index - texR * texNumC;\n  return (vec2(texC, texR) + halfCR) / vec2(texNumC, texNumR);\n}\n";
+var SAMPLE_4D_SNIPPET = "\nvec2 UVfrom4D(int texNumR, int texNumC, int stride0,\n    int stride1, int stride2, int row, int col, int depth,\n    int depth2) {\n  // Explicitly use integer operations as dot() only works on floats.\n  int index = row * stride0 + col * stride1 + depth * stride2 + depth2;\n  int texR = index / texNumC;\n  int texC = index - texR * texNumC;\n  return (vec2(texC, texR) + halfCR) / vec2(texNumC, texNumR);\n}\n";
+var UNSIGNED_BYTE_TEXTURE_SAMPLE_SNIPPET = "\n  uniform float NaN;\n\n  const vec4 floatDeltas = vec4(\n      1.0,\n      1.0 / 255.0,\n      1.0 / (255.0 * 255.0),\n      1.0 / (255.0 * 255.0 * 255.0)\n  );\n  const float minValue = " + tex_util.FLOAT_MIN + ".0;\n  const float maxValue = " + tex_util.FLOAT_MAX + ".0;\n  const float range = (maxValue - minValue) / 255.0;\n  const vec2 dotRange = vec2(1.0, range);\n\n  float sample(sampler2D texture, vec2 uv) {\n    vec4 sampleValue = texture2D(texture, uv);\n    if (all(equal(sampleValue, vec4(" + tex_util.BYTE_NAN_VALUE + ")))) {\n      return NaN;\n    }\n\n    vec4 encValue = floor(sampleValue * 255.0 + 0.5);\n    float decodedValue = dot(encValue, floatDeltas);\n    return dot(vec2(minValue, decodedValue), dotRange);\n  }\n";
+var UNSIGNED_BYTE_TEXTURE_SETOUTPUT_SNIPPET = "\n  const vec4 floatPowers = vec4(\n    1.0,\n    255.0,\n    255.0 * 255.0,\n    255.0 * 255.0 * 255.0\n  );\n  const vec2 recipRange = vec2(1.0/range);\n  const vec2 recipRange255 = vec2(1.0/(maxValue - minValue));\n\n  void setOutput(float decodedValue) {\n    if (isNaN(decodedValue)) {\n      gl_FragColor = vec4(" + tex_util.BYTE_NAN_VALUE + ");\n      return;\n    }\n\n    float a = dot(vec2(decodedValue, -minValue), recipRange);\n    float b = fract(a) * 255.0;\n    float c = fract(b) * 255.0;\n    float d = fract(c) * 255.0;\n    gl_FragColor = floor(vec4(a, b, c, d)) / 255.0;\n\n    // TODO(dsmilkov): Version above gets better accuracy but probably slower\n    // than the version below. Benchmark to determine if the accuracy is worth\n    // the cost.\n\n    // float normValue = dot(vec2(decodedValue, -minValue), recipRange255);\n    // vec4 f = normValue * floatPowers;\n    // gl_FragColor = floor(fract(f) * 255.0) / 255.0;\n  }\n";
+var FLOAT_TEXTURE_SAMPLE_SNIPPET = "\n  float sample(sampler2D texture, vec2 uv) {\n    return texture2D(texture, uv).r;\n  }\n";
+var FLOAT_TEXTURE_SETOUTPUT_SNIPPET = "\n  void setOutput(float val) {\n    gl_FragColor = vec4(val, 0, 0, 0);\n  }\n";
+var SHADER_PREFIX = "\n  precision highp float;\n  precision highp int;\n  varying vec2 resultUV;\n  const vec2 halfCR = vec2(0.5, 0.5);\n\n  bool isNaN(float val) {\n    float v1 = val * val;\n    float v2 = val * val;\n    return v1 == v2 ? false : true;\n  }\n\n  bool hasNaN(vec4 values) {\n    vec4 v1 = values * values;\n    vec4 v2 = values * values;\n    return any(notEqual(v1, v2));\n  }\n\n  float getNaN(vec4 values) {\n    return dot(vec4(1), values);\n  }\n\n  int round(float value) {\n    return int(floor(value + 0.5));\n  }\n\n  int imod(int x, int y) {\n    return x - y * (x / y);\n  }\n\n  const vec2 randomConst = vec2(\n    23.14069263277926, // e^pi (Gelfond's constant)\n     2.665144142690225 // 2^sqrt(2) (Gelfond\u2013Schneider constant)\n  );\n\n  float random(float seed) {\n      return fract(cos(dot(resultUV * seed, randomConst)) * 12345.6789);\n  }\n\n  float sampleUVAndDepth(sampler2D texture, vec2 uv, int depth) {\n    float value;\n    if (depth == 0) {\n      value = texture2D(texture, uv).r;\n    } else if (depth == 1) {\n      value = texture2D(texture, uv).g;\n    } else if (depth == 2) {\n      value = texture2D(texture, uv).b;\n    } else if (depth == 3) {\n      value = texture2D(texture, uv).a;\n    }\n    return floor(value * 255.0 + 0.5);\n  }\n\n  " + SAMPLE_1D_SNIPPET + "\n  " + SAMPLE_2D_SNIPPET + "\n  " + SAMPLE_3D_SNIPPET + "\n  " + SAMPLE_4D_SNIPPET + "\n";
+function getOutputScalarCoords() {
+    return "\n    int getOutputCoords() {\n      return 0;\n    }\n  ";
+}
+function getOutput1DCoords(shape, texShape) {
+    if (texShape[0] === 1) {
+        return "\n      int getOutputCoords() {\n        return int(resultUV.x * " + texShape[1] + ".0);\n      }\n    ";
+    }
+    if (texShape[1] === 1) {
+        return "\n      int getOutputCoords() {\n        return int(resultUV.y * " + texShape[0] + ".0);\n      }\n    ";
+    }
+    return "\n    int getOutputCoords() {\n      ivec2 resTexRC = ivec2(resultUV.yx *\n                             vec2(" + texShape[0] + ", " + texShape[1] + "));\n      return resTexRC.x * " + texShape[1] + " + resTexRC.y;\n    }\n  ";
+}
+function getOutput3DCoords(shape, texShape) {
+    var stride0 = shape[1] * shape[2];
+    var stride1 = shape[2];
+    return "\n    ivec3 getOutputCoords() {\n      ivec2 resTexRC = ivec2(resultUV.yx *\n                             vec2(" + texShape[0] + ", " + texShape[1] + "));\n      int index = resTexRC.x * " + texShape[1] + " + resTexRC.y;\n      int r = index / " + stride0 + ";\n      index -= r * " + stride0 + ";\n      int c = index / " + stride1 + ";\n      int d = index - c * " + stride1 + ";\n      return ivec3(r, c, d);\n    }\n  ";
+}
+function getOutput4DCoords(shape, texShape) {
+    var stride2 = shape[3];
+    var stride1 = shape[2] * stride2;
+    var stride0 = shape[1] * stride1;
+    return "\n    ivec4 getOutputCoords() {\n      ivec2 resTexRC = ivec2(resultUV.yx *\n        vec2(" + texShape[0] + ", " + texShape[1] + "));\n      int index = resTexRC.x * " + texShape[1] + " + resTexRC.y;\n\n      int r = index / " + stride0 + ";\n      index -= r * " + stride0 + ";\n\n      int c = index / " + stride1 + ";\n      index -= c * " + stride1 + ";\n\n      int d = index / " + stride2 + ";\n      int d2 = index - d * " + stride2 + ";\n\n      return ivec4(r, c, d, d2);\n    }\n  ";
+}
+function getOutput2DCoords(shape, texShape) {
+    if (util.arraysEqual(shape, texShape)) {
+        return "\n      ivec2 getOutputCoords() {\n        return ivec2(resultUV.yx * vec2(" + texShape[0] + ", " + texShape[1] + "));\n      }\n    ";
+    }
+    if (shape[1] === 1) {
+        return "\n      ivec2 getOutputCoords() {\n        ivec2 resTexRC = ivec2(resultUV.yx *\n                               vec2(" + texShape[0] + ", " + texShape[1] + "));\n        int index = resTexRC.x * " + texShape[1] + " + resTexRC.y;\n        return ivec2(index, 0);\n      }\n    ";
+    }
+    if (shape[0] === 1) {
+        return "\n      ivec2 getOutputCoords() {\n        ivec2 resTexRC = ivec2(resultUV.yx *\n                               vec2(" + texShape[0] + ", " + texShape[1] + "));\n        int index = resTexRC.x * " + texShape[1] + " + resTexRC.y;\n        return ivec2(0, index);\n      }\n    ";
+    }
+    return "\n    ivec2 getOutputCoords() {\n      ivec2 resTexRC = ivec2(resultUV.yx *\n                             vec2(" + texShape[0] + ", " + texShape[1] + "));\n      int index = resTexRC.x * " + texShape[1] + " + resTexRC.y;\n      int r = index / " + shape[1] + ";\n      int c = index - r * " + shape[1] + ";\n      return ivec2(r, c);\n    }\n  ";
+}
+function getSamplerScalar(inputInfo) {
+    var texName = inputInfo.name;
+    var funcName = 'get' + texName.charAt(0).toUpperCase() + texName.slice(1);
+    return "\n    float " + funcName + "() {\n      return sample(" + texName + ", halfCR);\n    }\n  ";
+}
+function getSampler1D(inputInfo) {
+    var texName = inputInfo.name;
+    var funcName = 'get' + texName.charAt(0).toUpperCase() + texName.slice(1);
+    return "\n    float " + funcName + "(int index) {\n      return " + funcName + "Flat(index);\n    }\n  ";
+}
+function getSampler2D(inputInfo) {
+    var shape = inputInfo.shapeInfo.logicalShape;
+    var texShape = inputInfo.shapeInfo.texShape;
+    var texName = inputInfo.name;
+    var funcName = 'get' + texName.charAt(0).toUpperCase() + texName.slice(1);
+    var texNumR = texShape[0];
+    var texNumC = texShape[1];
+    if (util.arraysEqual(shape, texShape)) {
+        return "\n    float " + funcName + "(int row, int col) {\n      vec2 uv = (vec2(col, row) + halfCR) / vec2(" + texNumC + ".0, " + texNumR + ".0);\n      return sample(" + texName + ", uv);\n    }\n  ";
+    }
+    var _a = util.squeezeShape(shape), newShape = _a.newShape, keptDims = _a.keptDims;
+    var squeezedShape = newShape;
+    if (squeezedShape.length < shape.length) {
+        var newInputInfo = squeezeInputInfo(inputInfo, squeezedShape);
+        var params = ['row', 'col'];
+        return "\n      " + getSamplerFromInInfo(newInputInfo) + "\n      float " + funcName + "(int row, int col) {\n        return " + funcName + "(" + getSqueezedParams(params, keptDims) + ");\n      }\n    ";
+    }
+    if (texNumC === 1) {
+        return "\n    float " + funcName + "(int row, int col) {\n      int index = row * " + shape[1] + " + col;\n      vec2 uv = vec2(0.5, (float(index) + 0.5) / " + texNumR + ".0);\n      return sample(" + texName + ", uv);\n    }\n  ";
+    }
+    if (texNumR === 1) {
+        return "\n    float " + funcName + "(int row, int col) {\n      int index = row * " + shape[1] + " + col;\n      vec2 uv = vec2((float(index) + 0.5) / " + texNumC + ".0, 0.5);\n      return sample(" + texName + ", uv);\n    }\n  ";
+    }
+    return "\n  float " + funcName + "(int row, int col) {\n    vec2 uv = UVfrom2D(" + texNumR + ", " + texNumC + ", " + shape[1] + ", row, col);\n    return sample(" + texName + ", uv);\n  }\n";
+}
+function getSampler3D(inputInfo) {
+    var texShape = inputInfo.shapeInfo.texShape;
+    var shape = inputInfo.shapeInfo.logicalShape;
+    var texName = inputInfo.name;
+    var funcName = 'get' + texName.charAt(0).toUpperCase() + texName.slice(1);
+    var texNumR = texShape[0];
+    var texNumC = texShape[1];
+    var stride0 = shape[1] * shape[2];
+    var stride1 = shape[2];
+    var texType = inputInfo.shapeInfo.textureType;
+    if (texType === tex_util_1.TextureType.DEFAULT) {
+        var _a = util.squeezeShape(shape), newShape = _a.newShape, keptDims = _a.keptDims;
+        var squeezedShape = newShape;
+        if (squeezedShape.length < shape.length) {
+            var newInputInfo = squeezeInputInfo(inputInfo, squeezedShape);
+            var params = ['row', 'col', 'depth'];
+            return "\n        " + getSamplerFromInInfo(newInputInfo) + "\n        float " + funcName + "(int row, int col, int depth) {\n          return " + funcName + "(" + getSqueezedParams(params, keptDims) + ");\n        }\n      ";
+        }
+    }
+    if (texNumC === stride0) {
+        if (texType === tex_util_1.TextureType.DEFAULT) {
+            return "\n        float " + funcName + "(int row, int col, int depth) {\n          int texR = row;\n          int texC = col * " + stride1 + " + depth;\n          vec2 uv = (vec2(texC, texR) + halfCR) /\n                     vec2(" + texNumC + ".0, " + texNumR + ".0);\n          return sample(" + texName + ", uv);\n        }\n      ";
+        }
+        else if (texType === tex_util_1.TextureType.RGBA_COLOR) {
+            return "\n        float " + funcName + "(int row, int col, int depth) {\n          vec2 uv = (vec2(col, row) + halfCR) /\n                     vec2(" + texNumC + ".0, " + texNumR + ".0);\n          return sampleUVAndDepth(" + texName + ", uv, depth);\n        }\n      ";
+        }
+        else {
+            throw new Error("Unknown TextureType " + texType + ".");
+        }
+    }
+    if (texNumC === stride1 && texType === tex_util_1.TextureType.DEFAULT) {
+        return "\n    float " + funcName + "(int row, int col, int depth) {\n      int texR = row * " + shape[1] + " + col;\n      int texC = depth;\n      vec2 uv = (vec2(texC, texR) + halfCR) / vec2(" + texNumC + ".0, " + texNumR + ".0);\n      return sample(" + texName + ", uv);\n    }\n  ";
+    }
+    if (texType === tex_util_1.TextureType.DEFAULT) {
+        return "\n      float " + funcName + "(int row, int col, int depth) {\n        vec2 uv = UVfrom3D(\n            " + texNumR + ", " + texNumC + ", " + stride0 + ", " + stride1 + ", row, col, depth);\n        return sample(" + texName + ", uv);\n      }\n  ";
+    }
+    else if (texType === tex_util_1.TextureType.RGBA_COLOR) {
+        return "\n      float " + funcName + "(int row, int col, int depth) {\n        vec2 uv = UVfrom2D(" + texNumR + ", " + texNumC + ", " + shape[1] + ", row, col);\n        return sampleUVAndDepth(" + texName + ", uv, depth);\n      }\n    ";
+    }
+    else {
+        throw new Error("Unknown TextureType " + texType + ".");
+    }
+}
+function getSampler4D(inputInfo) {
+    var shape = inputInfo.shapeInfo.logicalShape;
+    var texShape = inputInfo.shapeInfo.texShape;
+    var texName = inputInfo.name;
+    var funcName = 'get' + texName.charAt(0).toUpperCase() + texName.slice(1);
+    var texNumR = texShape[0];
+    var texNumC = texShape[1];
+    var stride2 = shape[3];
+    var stride1 = shape[2] * stride2;
+    var stride0 = shape[1] * stride1;
+    var _a = util.squeezeShape(shape), newShape = _a.newShape, keptDims = _a.keptDims;
+    if (newShape.length < shape.length) {
+        var newInputInfo = squeezeInputInfo(inputInfo, newShape);
+        var params = ['row', 'col', 'depth', 'depth2'];
+        return "\n      " + getSamplerFromInInfo(newInputInfo) + "\n      float " + funcName + "(int row, int col, int depth, int depth2) {\n        return " + funcName + "(" + getSqueezedParams(params, keptDims) + ");\n      }\n    ";
+    }
+    if (texNumC === stride0) {
+        return "\n      float " + funcName + "(int row, int col, int depth, int depth2) {\n        int texR = row;\n        int texC = col * " + stride1 + " + depth * " + stride2 + " + depth2;\n        vec2 uv = (vec2(texC, texR) + halfCR) /\n                   vec2(" + texNumC + ".0, " + texNumR + ".0);\n        return sample(" + texName + ", uv);\n      }\n    ";
+    }
+    if (texNumC === stride2) {
+        return "\n      float " + funcName + "(int row, int col, int depth, int depth2) {\n        int texR = row * " + shape[1] * shape[2] + " + col * " + shape[2] + " + depth;\n        int texC = depth2;\n        vec2 uv = (vec2(texC, texR) + halfCR) /\n                  vec2(" + texNumC + ".0, " + texNumR + ".0);\n        return sample(" + texName + ", uv);\n      }\n    ";
+    }
+    return "\n    float " + funcName + "(int row, int col, int depth, int depth2) {\n      vec2 uv = UVfrom4D(" + texNumR + ", " + texNumC + ", " + stride0 + ", " + stride1 + ",\n          " + stride2 + ", row, col, depth, depth2);\n      return sample(" + texName + ", uv);\n    }\n  ";
+}
+function getSamplerFlat(inputInfo) {
+    var texName = inputInfo.name;
+    var texShape = inputInfo.shapeInfo.texShape;
+    var funcName = 'get' + texName.charAt(0).toUpperCase() + texName.slice(1) + 'Flat';
+    var tNumR = texShape[0];
+    var tNumC = texShape[1];
+    if (tNumC === 1 && tNumR === 1) {
+        return "\n      float " + funcName + "(int index) {\n        return sample(" + texName + ", halfCR);\n      }\n    ";
+    }
+    if (tNumC === 1) {
+        return "\n      float " + funcName + "(int index) {\n        vec2 uv = vec2(0.5, (float(index) + 0.5) / " + tNumR + ".0);\n        return sample(" + texName + ", uv);\n      }\n    ";
+    }
+    if (tNumR === 1) {
+        return "\n      float " + funcName + "(int index) {\n        vec2 uv = vec2((float(index) + 0.5) / " + tNumC + ".0, 0.5);\n        return sample(" + texName + ", uv);\n      }\n    ";
+    }
+    return "\n    float " + funcName + "(int index) {\n      vec2 uv = UVfrom1D(" + tNumR + ", " + tNumC + ", index);\n      return sample(" + texName + ", uv);\n    }\n  ";
+}
+function getBroadcastOutputCoordsSampler(inputInfo, outShapeInfo, texFuncSnippet, funcName) {
+    var inRank = inputInfo.shapeInfo.logicalShape.length;
+    var outRank = outShapeInfo.logicalShape.length;
+    var type = 'int';
+    if (outRank === 2) {
+        type = 'ivec2';
+    }
+    else if (outRank === 3) {
+        type = 'ivec3';
+    }
+    else if (outRank === 4) {
+        type = 'ivec4';
+    }
+    var broadcastDims = broadcast_util.getBroadcastDims(inputInfo.shapeInfo.logicalShape, outShapeInfo.logicalShape);
+    var rankDiff = outRank - inRank;
+    var coordsSnippet;
+    if (inRank === 0) {
+        coordsSnippet = '';
+    }
+    else if (outRank < 2 && broadcastDims.length >= 1) {
+        coordsSnippet = 'coords = 0;';
+    }
+    else {
+        coordsSnippet =
+            broadcastDims.map(function (d) { return "coords[" + (d + rankDiff) + "] = 0;"; }).join('\n');
+    }
+    var unpackedCoordsSnippet = '';
+    if (outRank < 2 && inRank > 0) {
+        unpackedCoordsSnippet = 'coords';
+    }
+    else {
+        unpackedCoordsSnippet = inputInfo.shapeInfo.logicalShape
+            .map(function (s, i) { return "coords[" + (i + rankDiff) + "]"; })
+            .join(', ');
+    }
+    return "\n    float " + funcName + "() {\n      " + type + " coords = getOutputCoords();\n      " + coordsSnippet + "\n      return get" + texFuncSnippet + "(" + unpackedCoordsSnippet + ");\n    }\n  ";
+}
+function getSamplerAtOutputCoords(inputInfo, outShapeInfo, supportsBroadcasting) {
+    var inTexShape = inputInfo.shapeInfo.texShape;
+    var texName = inputInfo.name;
+    var isRGBAColorTexture = inputInfo.shapeInfo.textureType === tex_util_1.TextureType.RGBA_COLOR;
+    var texFuncSnippet = texName.charAt(0).toUpperCase() + texName.slice(1);
+    var funcName = 'get' + texFuncSnippet + 'AtOutCoords';
+    var broadcastDims = broadcast_util.getBroadcastDims(inputInfo.shapeInfo.logicalShape, outShapeInfo.logicalShape);
+    var inRank = inputInfo.shapeInfo.logicalShape.length;
+    var outRank = outShapeInfo.logicalShape.length;
+    var doBroadcast = supportsBroadcasting && ((outRank > inRank) || broadcastDims.length > 0);
+    var broadcastOverOuter = broadcast_util.broadcastDimsAreOuter(broadcastDims);
+    if (doBroadcast && !broadcastOverOuter) {
+        return getBroadcastOutputCoordsSampler(inputInfo, outShapeInfo, texFuncSnippet, funcName);
+    }
+    var outTexShape = outShapeInfo.texShape;
+    if (util.arraysEqual(inTexShape, outTexShape) && !isRGBAColorTexture) {
+        return "\n      float " + funcName + "() {\n        return sample(" + texName + ", resultUV);\n      }\n    ";
+    }
+    var inTexExpandedShape = isRGBAColorTexture ?
+        [inTexShape[0], inTexShape[1] * inputInfo.shapeInfo.logicalShape[2]] :
+        inTexShape;
+    var sampleSnippet = "return sample(" + texName + ", uv);";
+    var rgbaColorSnippet = '';
+    if (isRGBAColorTexture) {
+        rgbaColorSnippet = "\n      int col = texC / " + inputInfo.shapeInfo.logicalShape[2] + ";\n      int texD = texC - col * " + inputInfo.shapeInfo.logicalShape[2] + ";\n      texC = col;\n    ";
+        sampleSnippet = "return sampleUVAndDepth(" + texName + ", uv, texD);";
+    }
+    var inSize = util.sizeFromShape(inTexExpandedShape);
+    var broadcastSnippet = '';
+    if (doBroadcast && broadcastOverOuter) {
+        broadcastSnippet = "\n        int mainPart = index / " + inSize + ";\n        index -= mainPart * " + inSize + ";\n      ";
+    }
+    return "\n    float " + funcName + "() {\n      ivec2 resTexRC = ivec2(resultUV.yx *\n                             vec2(" + outTexShape[0] + ", " + outTexShape[1] + "));\n      int index = resTexRC.x * " + outTexShape[1] + " + resTexRC.y;\n      " + broadcastSnippet + "\n      int texR = index / " + inTexExpandedShape[1] + ";\n      int texC = index - texR * " + inTexExpandedShape[1] + ";\n\n      " + rgbaColorSnippet + "\n\n      vec2 uv = (vec2(texC, texR) + halfCR) /\n                 vec2(" + inTexShape[1] + ".0, " + inTexShape[0] + ".0);\n\n      " + sampleSnippet + "\n    }\n  ";
+}
+function getCoordsDataType(rank) {
+    if (rank === 1) {
+        return 'int';
+    }
+    else if (rank === 2) {
+        return 'ivec2';
+    }
+    else if (rank === 3) {
+        return 'ivec3';
+    }
+    else if (rank === 4) {
+        return 'ivec4';
+    }
+    else {
+        throw Error("GPU for rank " + rank + " is not yet supported");
+    }
+}
+exports.getCoordsDataType = getCoordsDataType;
+function squeezeInputInfo(inInfo, squeezedShape) {
+    var newInputInfo = JSON.parse(JSON.stringify(inInfo));
+    newInputInfo.shapeInfo.logicalShape = squeezedShape;
+    return newInputInfo;
+}
+function getSqueezedParams(params, keptDims) {
+    return keptDims.map(function (d) { return params[d]; }).join(', ');
+}
+
+},{"../../../environment":15,"../../../util":101,"../../broadcast_util":90,"./tex_util":84}],83:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var shader_compiler_1 = require("./shader_compiler");
+var SliceProgram = (function () {
+    function SliceProgram(destSize) {
+        this.variableNames = ['source'];
+        this.outputShape = destSize;
+        this.rank = destSize.length;
+        var dtype = shader_compiler_1.getCoordsDataType(this.rank);
+        var sourceCoords = getCoords(this.rank);
+        this.userCode = "\n      uniform " + dtype + " start;\n\n      void main() {\n        " + dtype + " sourceLoc = start + getOutputCoords();\n        setOutput(getSource(" + sourceCoords + "));\n      }\n    ";
+    }
+    SliceProgram.prototype.getCustomSetupFunc = function (start) {
+        var _this = this;
+        if (start.length !== this.rank) {
+            throw Error("The rank (" + this.rank + ") of the program must match the " +
+                ("length of start (" + start.length + ")"));
+        }
+        return function (gpgpu, webGLProgram) {
+            if (_this.startLoc == null) {
+                _this.startLoc = gpgpu.getUniformLocationNoThrow(webGLProgram, 'start');
+                if (_this.startLoc == null) {
+                    return;
+                }
+            }
+            if (_this.rank === 1) {
+                gpgpu.gl.uniform1i(_this.startLoc, start[0]);
+            }
+            else if (_this.rank === 2) {
+                gpgpu.gl.uniform2i(_this.startLoc, start[0], start[1]);
+            }
+            else if (_this.rank === 3) {
+                gpgpu.gl.uniform3i(_this.startLoc, start[0], start[1], start[2]);
+            }
+            else if (_this.rank === 4) {
+                gpgpu.gl.uniform4i(_this.startLoc, start[0], start[1], start[2], start[3]);
+            }
+            else {
+                throw Error("Slicing for rank " + _this.rank + " is not yet supported");
+            }
+        };
+    };
+    return SliceProgram;
+}());
+exports.SliceProgram = SliceProgram;
+function getCoords(rank) {
+    if (rank === 1) {
+        return 'sourceLoc';
+    }
+    else if (rank === 2) {
+        return 'sourceLoc.x, sourceLoc.y';
+    }
+    else if (rank === 3) {
+        return 'sourceLoc.x, sourceLoc.y, sourceLoc.z';
+    }
+    else if (rank === 4) {
+        return 'sourceLoc.x, sourceLoc.y, sourceLoc.z, sourceLoc.w';
+    }
+    else {
+        throw Error("Slicing for rank " + rank + " is not yet supported");
+    }
+}
+
+},{"./shader_compiler":82}],84:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var TextureType;
+(function (TextureType) {
+    TextureType[TextureType["DEFAULT"] = 0] = "DEFAULT";
+    TextureType[TextureType["RGBA_COLOR"] = 1] = "RGBA_COLOR";
+})(TextureType = exports.TextureType || (exports.TextureType = {}));
+function getUnpackedMatrixTextureShapeWidthHeight(rows, columns) {
+    return [columns, rows];
+}
+exports.getUnpackedMatrixTextureShapeWidthHeight = getUnpackedMatrixTextureShapeWidthHeight;
+function getUnpackedArraySizeFromMatrixSize(matrixSize, channelsPerTexture) {
+    return matrixSize * channelsPerTexture;
+}
+exports.getUnpackedArraySizeFromMatrixSize = getUnpackedArraySizeFromMatrixSize;
+function getColorMatrixTextureShapeWidthHeight(rows, columns) {
+    return [columns * 4, rows];
+}
+exports.getColorMatrixTextureShapeWidthHeight = getColorMatrixTextureShapeWidthHeight;
+function getMatrixSizeFromUnpackedArraySize(unpackedSize, channelsPerTexture) {
+    if (unpackedSize % channelsPerTexture !== 0) {
+        throw new Error("unpackedSize (" + unpackedSize + ") must be a multiple of " +
+            ("" + channelsPerTexture));
+    }
+    return unpackedSize / channelsPerTexture;
+}
+exports.getMatrixSizeFromUnpackedArraySize = getMatrixSizeFromUnpackedArraySize;
+function encodeMatrixToUnpackedArray(matrix, unpackedArray, channelsPerTexture) {
+    var requiredSize = getUnpackedArraySizeFromMatrixSize(matrix.length, channelsPerTexture);
+    if (unpackedArray.length < requiredSize) {
+        throw new Error("unpackedArray length (" + unpackedArray.length + ") must be >= " +
+            ("" + requiredSize));
+    }
+    var dst = 0;
+    for (var src = 0; src < matrix.length; ++src) {
+        unpackedArray[dst] = matrix[src];
+        dst += channelsPerTexture;
+    }
+}
+exports.encodeMatrixToUnpackedArray = encodeMatrixToUnpackedArray;
+exports.FLOAT_MAX = 20000;
+exports.FLOAT_MIN = -exports.FLOAT_MAX;
+var FLOAT_RANGE = (exports.FLOAT_MAX - exports.FLOAT_MIN) / 255;
+var FLOAT_DELTAS = [1, 1 / 255, 1 / (255 * 255), 1 / (255 * 255 * 255)];
+var FLOAT_POWERS = [1, 255, 255 * 255];
+exports.BYTE_NAN_VALUE = 0;
+function encodeFloatArray(floatArray) {
+    var uintArray = new Uint8Array(floatArray.length * 4);
+    var _loop_1 = function (i) {
+        var value = floatArray[i / 4];
+        if (isNaN(value)) {
+            uintArray[i] = exports.BYTE_NAN_VALUE;
+            uintArray[i + 1] = exports.BYTE_NAN_VALUE;
+            uintArray[i + 2] = exports.BYTE_NAN_VALUE;
+            uintArray[i + 3] = exports.BYTE_NAN_VALUE;
+            return "continue";
+        }
+        var normalizedValue = (value - exports.FLOAT_MIN) / FLOAT_RANGE;
+        var enc = FLOAT_POWERS.map(function (pow) { return pow * normalizedValue; });
+        var buckets = enc.map(function (value) { return Math.floor((value % 1) * 255); });
+        uintArray[i] = Math.floor(normalizedValue);
+        uintArray[i + 1] = buckets[0];
+        uintArray[i + 2] = buckets[1];
+        uintArray[i + 3] = buckets[2];
+    };
+    for (var i = 0; i < uintArray.length; i += 4) {
+        _loop_1(i);
+    }
+    return uintArray;
+}
+exports.encodeFloatArray = encodeFloatArray;
+function decodeToFloatArray(uintArray) {
+    var floatArray = new Float32Array(uintArray.length / 4);
+    var _loop_2 = function (i) {
+        if (uintArray[i] === exports.BYTE_NAN_VALUE &&
+            uintArray[i + 1] === exports.BYTE_NAN_VALUE &&
+            uintArray[i + 2] === exports.BYTE_NAN_VALUE &&
+            uintArray[i + 3] === exports.BYTE_NAN_VALUE) {
+            floatArray[i / 4] = NaN;
+            return "continue";
+        }
+        var dot = 0;
+        FLOAT_DELTAS.forEach(function (delta, j) {
+            dot += delta * uintArray[i + j];
+        });
+        var value = dot * FLOAT_RANGE + exports.FLOAT_MIN;
+        floatArray[i / 4] = value;
+    };
+    for (var i = 0; i < uintArray.length; i += 4) {
+        _loop_2(i);
+    }
+    return floatArray;
+}
+exports.decodeToFloatArray = decodeToFloatArray;
+function decodeMatrixFromUnpackedArray(unpackedArray, matrix, channelsPerTexture) {
+    var requiredSize = getMatrixSizeFromUnpackedArraySize(unpackedArray.length, channelsPerTexture);
+    if (matrix.length < requiredSize) {
+        throw new Error("matrix length (" + matrix.length + ") must be >= " + requiredSize);
+    }
+    var dst = 0;
+    for (var src = 0; src < unpackedArray.length; src += channelsPerTexture) {
+        matrix[dst++] = unpackedArray[src];
+    }
+}
+exports.decodeMatrixFromUnpackedArray = decodeMatrixFromUnpackedArray;
+function decodeMatrixFromUnpackedColorRGBAArray(unpackedArray, matrix, channels) {
+    var requiredSize = unpackedArray.length * channels / 4;
+    if (matrix.length < requiredSize) {
+        throw new Error("matrix length (" + matrix.length + ") must be >= " + requiredSize);
+    }
+    var dst = 0;
+    for (var src = 0; src < unpackedArray.length; src += 4) {
+        for (var c = 0; c < channels; c++) {
+            matrix[dst++] = unpackedArray[src + c];
+        }
+    }
+}
+exports.decodeMatrixFromUnpackedColorRGBAArray = decodeMatrixFromUnpackedColorRGBAArray;
+function getPackedMatrixTextureShapeWidthHeight(rows, columns) {
+    return [Math.ceil(columns / 2), Math.ceil(rows / 2)];
+}
+exports.getPackedMatrixTextureShapeWidthHeight = getPackedMatrixTextureShapeWidthHeight;
+function getPackedRGBAArraySizeFromMatrixShape(rows, columns) {
+    var _a = getPackedMatrixTextureShapeWidthHeight(rows, columns), w = _a[0], h = _a[1];
+    return w * h * 4;
+}
+exports.getPackedRGBAArraySizeFromMatrixShape = getPackedRGBAArraySizeFromMatrixShape;
+function encodeMatrixToPackedRGBA(matrix, rows, columns, packedRGBA) {
+    var requiredSize = getPackedRGBAArraySizeFromMatrixShape(rows, columns);
+    if (packedRGBA.length < requiredSize) {
+        throw new Error("packedRGBA length (" + packedRGBA.length + ") must be >= " + requiredSize);
+    }
+    var _a = getPackedMatrixTextureShapeWidthHeight(rows, columns), textureWidth = _a[0], textureHeight = _a[1];
+    var oddWidth = (columns % 2) === 1;
+    var oddHeight = (rows % 2) === 1;
+    var widthInFullBlocks = Math.floor(columns / 2);
+    var heightInFullBlocks = Math.floor(rows / 2);
+    {
+        var dstStride = (oddWidth ? 4 : 0);
+        var oneRow = columns;
+        var dst = 0;
+        for (var blockY = 0; blockY < heightInFullBlocks; ++blockY) {
+            var matrixSrcRow = (blockY * 2 * columns);
+            for (var blockX = 0; blockX < widthInFullBlocks; ++blockX) {
+                var matrixSrcCol = blockX * 2;
+                var src = matrixSrcRow + matrixSrcCol;
+                packedRGBA[dst] = matrix[src];
+                packedRGBA[dst + 1] = matrix[src + 1];
+                packedRGBA[dst + 2] = matrix[src + oneRow];
+                packedRGBA[dst + 3] = matrix[src + oneRow + 1];
+                dst += 4;
+            }
+            dst += dstStride;
+        }
+    }
+    if (oddWidth) {
+        var src = columns - 1;
+        var dst = (textureWidth - 1) * 4;
+        var srcStride = 2 * columns;
+        var dstStride = textureWidth * 4;
+        for (var blockY = 0; blockY < heightInFullBlocks; ++blockY) {
+            packedRGBA[dst] = matrix[src];
+            packedRGBA[dst + 2] = matrix[src + columns];
+            src += srcStride;
+            dst += dstStride;
+        }
+    }
+    if (oddHeight) {
+        var src = (rows - 1) * columns;
+        var dst = (textureHeight - 1) * textureWidth * 4;
+        for (var blockX = 0; blockX < widthInFullBlocks; ++blockX) {
+            packedRGBA[dst++] = matrix[src++];
+            packedRGBA[dst++] = matrix[src++];
+            dst += 2;
+        }
+    }
+    if (oddWidth && oddHeight) {
+        packedRGBA[packedRGBA.length - 4] = matrix[matrix.length - 1];
+    }
+    return packedRGBA;
+}
+exports.encodeMatrixToPackedRGBA = encodeMatrixToPackedRGBA;
+function decodeMatrixFromPackedRGBA(packedRGBA, rows, columns, matrix) {
+    var requiredSize = rows * columns;
+    if (requiredSize < matrix.length) {
+        throw new Error("matrix length (" + matrix.length + ") must be >= " + requiredSize);
+    }
+    var oddWidth = (columns % 2) === 1;
+    var oddHeight = (rows % 2) === 1;
+    var widthInFullBlocks = Math.floor(columns / 2);
+    var heightInFullBlocks = Math.floor(rows / 2);
+    var _a = getPackedMatrixTextureShapeWidthHeight(rows, columns), textureWidth = _a[0], textureHeight = _a[1];
+    {
+        var srcStride = oddWidth ? 4 : 0;
+        var dstStride = columns + (oddWidth ? 1 : 0);
+        var src = 0;
+        var dstRow1 = 0;
+        var dstRow2 = columns;
+        for (var blockY = 0; blockY < heightInFullBlocks; ++blockY) {
+            for (var blockX = 0; blockX < widthInFullBlocks; ++blockX) {
+                matrix[dstRow1++] = packedRGBA[src++];
+                matrix[dstRow1++] = packedRGBA[src++];
+                matrix[dstRow2++] = packedRGBA[src++];
+                matrix[dstRow2++] = packedRGBA[src++];
+            }
+            src += srcStride;
+            dstRow1 += dstStride;
+            dstRow2 += dstStride;
+        }
+    }
+    if (oddWidth) {
+        var src = (textureWidth - 1) * 4;
+        var dst = columns - 1;
+        var srcStride = textureWidth * 4;
+        var dstStride = 2 * columns;
+        for (var blockY = 0; blockY < heightInFullBlocks; ++blockY) {
+            matrix[dst] = packedRGBA[src];
+            matrix[dst + columns] = packedRGBA[src + 2];
+            src += srcStride;
+            dst += dstStride;
+        }
+    }
+    if (oddHeight) {
+        var src = (textureHeight - 1) * textureWidth * 4;
+        var dst = (rows - 1) * columns;
+        for (var blockX = 0; blockX < widthInFullBlocks; ++blockX) {
+            matrix[dst++] = packedRGBA[src++];
+            matrix[dst++] = packedRGBA[src++];
+            src += 2;
+        }
+    }
+    if (oddWidth && oddHeight) {
+        matrix[matrix.length - 1] = packedRGBA[packedRGBA.length - 4];
+    }
+    return matrix;
+}
+exports.decodeMatrixFromPackedRGBA = decodeMatrixFromPackedRGBA;
+
+},{}],85:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var TextureManager = (function () {
+    function TextureManager(gpgpu) {
+        this.gpgpu = gpgpu;
+        this.numUsedTextures = 0;
+        this.numFreeTextures = 0;
+        this.freeTextures = {};
+        this.logEnabled = false;
+        this.usedTextureCount = {};
+    }
+    TextureManager.prototype.acquireTexture = function (shapeRC) {
+        var shapeKey = getKeyFromTextureShape(shapeRC);
+        if (!(shapeKey in this.freeTextures)) {
+            this.freeTextures[shapeKey] = [];
+        }
+        if (!(shapeKey in this.usedTextureCount)) {
+            this.usedTextureCount[shapeKey] = 0;
+        }
+        this.usedTextureCount[shapeKey]++;
+        if (this.freeTextures[shapeKey].length > 0) {
+            this.numFreeTextures--;
+            this.numUsedTextures++;
+            this.log();
+            return this.freeTextures[shapeKey].shift();
+        }
+        this.numUsedTextures++;
+        this.log();
+        return this.gpgpu.createMatrixTexture(shapeRC[0], shapeRC[1]);
+    };
+    TextureManager.prototype.releaseTexture = function (texture, shape) {
+        var shapeKey = getKeyFromTextureShape(shape);
+        if (!(shapeKey in this.freeTextures)) {
+            this.freeTextures[shapeKey] = [];
+        }
+        this.freeTextures[shapeKey].push(texture);
+        this.numFreeTextures++;
+        this.numUsedTextures--;
+        this.usedTextureCount[shapeKey]--;
+        this.log();
+    };
+    TextureManager.prototype.log = function () {
+        if (!this.logEnabled) {
+            return;
+        }
+        var total = this.numFreeTextures + this.numUsedTextures;
+        console.log('Free/Used', this.numFreeTextures + " / " + this.numUsedTextures, "(" + total + ")");
+    };
+    TextureManager.prototype.getNumUsedTextures = function () {
+        return this.numUsedTextures;
+    };
+    TextureManager.prototype.getNumFreeTextures = function () {
+        return this.numFreeTextures;
+    };
+    TextureManager.prototype.dispose = function () {
+        for (var shape in this.freeTextures) {
+            if (this.freeTextures.hasOwnProperty(shape)) {
+                for (var i = 0; i < this.freeTextures[shape].length; i++) {
+                    this.gpgpu.deleteMatrixTexture(this.freeTextures[shape][i]);
+                }
+            }
+        }
+    };
+    return TextureManager;
+}());
+exports.TextureManager = TextureManager;
+function getKeyFromTextureShape(shapeRowsCol) {
+    return shapeRowsCol[0] + "_" + shapeRowsCol[1];
+}
+
+},{}],86:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var shader_compiler_1 = require("./shader_compiler");
+var TileProgram = (function () {
+    function TileProgram(aShape, reps) {
+        this.variableNames = ['A'];
+        var outputShape = new Array(aShape.length);
+        for (var i = 0; i < outputShape.length; i++) {
+            outputShape[i] = aShape[i] * reps[i];
+        }
+        this.outputShape = outputShape;
+        this.rank = outputShape.length;
+        var dtype = shader_compiler_1.getCoordsDataType(this.rank);
+        var sourceCoords = getSourceCoords(aShape);
+        this.userCode = "\n      void main() {\n        " + dtype + " resRC = getOutputCoords();\n        setOutput(getA(" + sourceCoords + "));\n      }\n    ";
+    }
+    return TileProgram;
+}());
+exports.TileProgram = TileProgram;
+function getSourceCoords(aShape) {
+    var rank = aShape.length;
+    if (rank > 4) {
+        throw Error("Tile for rank " + rank + " is not yet supported");
+    }
+    if (rank === 1) {
+        return "imod(resRC, " + aShape[0] + ")";
+    }
+    var currentCoords = ['resRC.x', 'resRC.y', 'resRC.z', 'resRC.w'];
+    var sourceCoords = [];
+    for (var i = 0; i < aShape.length; i++) {
+        sourceCoords.push("imod(" + currentCoords[i] + ", " + aShape[i] + ")");
+    }
+    return sourceCoords.join();
+}
+
+},{"./shader_compiler":82}],87:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var shader_compiler_1 = require("./shader_compiler");
+var TransposeProgram = (function () {
+    function TransposeProgram(aShape, newDim) {
+        this.variableNames = ['A'];
+        var outputShape = new Array(aShape.length);
+        for (var i = 0; i < outputShape.length; i++) {
+            outputShape[i] = aShape[newDim[i]];
+        }
+        this.outputShape = outputShape;
+        this.rank = outputShape.length;
+        var dtype = shader_compiler_1.getCoordsDataType(this.rank);
+        var switched = getSwitchedCoords(newDim);
+        this.userCode = "\n    void main() {\n      " + dtype + " resRC = getOutputCoords();\n      setOutput(getA(" + switched + "));\n    }\n    ";
+    }
+    return TransposeProgram;
+}());
+exports.TransposeProgram = TransposeProgram;
+function getSwitchedCoords(newDim) {
+    var rank = newDim.length;
+    if (rank > 4) {
+        throw Error("Transpose for rank " + rank + " is not yet supported");
+    }
+    var originalOrder = ['resRC.x', 'resRC.y', 'resRC.z', 'resRC.w'];
+    var switchedCoords = new Array(rank);
+    for (var i = 0; i < newDim.length; i++) {
+        switchedCoords[newDim[i]] = originalOrder[i];
+    }
+    return switchedCoords.join();
+}
+
+},{"./shader_compiler":82}],88:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var UnaryOpProgram = (function () {
+    function UnaryOpProgram(aShape, opSnippet) {
+        this.variableNames = ['A'];
+        this.outputShape = aShape;
+        this.userCode = "\n      float unaryOperation(float x) {\n        " + opSnippet + "\n      }\n\n      void main() {\n        float x = getAAtOutCoords();\n        float y = unaryOperation(x);\n\n        setOutput(y);\n      }\n    ";
+    }
+    return UnaryOpProgram;
+}());
+exports.UnaryOpProgram = UnaryOpProgram;
+exports.CHECK_NAN_SNIPPET = "\n  if (isNaN(x)) {\n    return x;\n  }\n";
+exports.ABS = "\n  return abs(x);\n";
+exports.RELU = exports.CHECK_NAN_SNIPPET + "\n  return (x < 0.0) ? 0.0 : x;\n";
+exports.ELU = "\n  return (x >= 0.0) ? x : (exp(x) - 1.0);\n";
+exports.ELU_DER = "\n  return (x >= 0.0) ? 1.0 : exp(x);\n";
+exports.SELU = "\n  // Stable and Attracting Fixed Point (0, 1) for Normalized Weights.\n  // see: https://arxiv.org/abs/1706.02515\n  float scaleAlpha = 1.7580993408473768599402175208123;\n  float scale = 1.0507009873554804934193349852946;\n  return (x >= 0.0) ? scale * x : scaleAlpha * (exp(x) - 1.0);\n";
+function LEAKY_RELU(alpha) {
+    return "\n    return (x >= 0.0) ? x : " + alpha + " * x;\n  ";
+}
+exports.LEAKY_RELU = LEAKY_RELU;
+function STEP(alpha) {
+    if (alpha === void 0) { alpha = 0.0; }
+    return exports.CHECK_NAN_SNIPPET + ("\n    return x > 0.0 ? 1.0 : float(" + alpha + ");\n  ");
+}
+exports.STEP = STEP;
+exports.NEG = "\n  return -x;\n";
+exports.CEIL = "\n  return ceil(x);\n";
+exports.FLOOR = "\n  return floor(x);\n";
+exports.EXP = "\n  return exp(x);\n";
+exports.LOG = "\n  return log(x);\n";
+exports.SQRT = exports.CHECK_NAN_SNIPPET + "\n  return sqrt(x);\n";
+exports.SIGMOID = "\n  return 1.0 / (1.0 + exp(-1.0 * x));\n";
+exports.SIN = exports.CHECK_NAN_SNIPPET + "\n  return sin(x);\n";
+exports.COS = exports.CHECK_NAN_SNIPPET + "\n  return cos(x);\n";
+exports.TAN = "\n  return tan(x);\n";
+exports.ASIN = exports.CHECK_NAN_SNIPPET + "\n  return asin(x);\n";
+exports.ACOS = exports.CHECK_NAN_SNIPPET + "\n  return acos(x);\n";
+exports.ATAN = exports.CHECK_NAN_SNIPPET + "\n  return atan(x);\n";
+exports.SINH = "\n  float e2x = exp(x);\n  return (e2x - 1.0 / e2x) / 2.0;\n";
+exports.COSH = "\n  float e2x = exp(-x);\n  return (e2x + 1.0 / e2x) / 2.0;\n";
+exports.TANH = "\n  float e2x = exp(-2.0 * abs(x));\n  return sign(x) * (1.0 - e2x) / (1.0 + e2x);\n";
+exports.SQUARE = "\n  return x * x;\n";
+
+},{}],89:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var MAX_TEXTURE_SIZE = null;
+var util = require("../../../util");
+var environment_1 = require("../../../environment");
+function createWebGLRenderingContext(attributes) {
+    var canvas = document.createElement('canvas');
+    canvas.width = 1;
+    canvas.height = 1;
+    return createWebGLRenderingContextFromCanvas(canvas, attributes);
+}
+exports.createWebGLRenderingContext = createWebGLRenderingContext;
+function createWebGLRenderingContextFromCanvas(canvas, attributes) {
+    var gl;
+    var webglVersion = environment_1.ENV.get('WEBGL_VERSION');
+    if (webglVersion === 2) {
+        gl = canvas.getContext('webgl2', attributes);
+    }
+    else if (webglVersion === 1) {
+        gl = (canvas.getContext('webgl', attributes) ||
+            canvas.getContext('experimental-webgl', attributes));
+    }
+    if (webglVersion === 0 || gl == null) {
+        throw new Error('This browser does not support WebGL.');
+    }
+    return gl;
+}
+exports.createWebGLRenderingContextFromCanvas = createWebGLRenderingContextFromCanvas;
+function callAndCheck(gl, func) {
+    var returnValue = func();
+    checkWebGLError(gl);
+    return returnValue;
+}
+exports.callAndCheck = callAndCheck;
+var webGLDebugErrorCheckingEnabled = false;
+function enableDebugWebGLErrorChecking(enabled) {
+    webGLDebugErrorCheckingEnabled = enabled;
+}
+exports.enableDebugWebGLErrorChecking = enableDebugWebGLErrorChecking;
+function checkWebGLError(gl) {
+    if (webGLDebugErrorCheckingEnabled) {
+        var error = gl.getError();
+        if (error !== gl.NO_ERROR) {
+            throw new Error('WebGL Error: ' + getWebGLErrorMessage(gl, error));
+        }
+    }
+}
+exports.checkWebGLError = checkWebGLError;
+function getWebGLErrorMessage(gl, status) {
+    switch (status) {
+        case gl.NO_ERROR:
+            return 'NO_ERROR';
+        case gl.INVALID_ENUM:
+            return 'INVALID_ENUM';
+        case gl.INVALID_VALUE:
+            return 'INVALID_VALUE';
+        case gl.INVALID_OPERATION:
+            return 'INVALID_OPERATION';
+        case gl.INVALID_FRAMEBUFFER_OPERATION:
+            return 'INVALID_FRAMEBUFFER_OPERATION';
+        case gl.OUT_OF_MEMORY:
+            return 'OUT_OF_MEMORY';
+        case gl.CONTEXT_LOST_WEBGL:
+            return 'CONTEXT_LOST_WEBGL';
+        default:
+            return "Unknown error code " + status;
+    }
+}
+exports.getWebGLErrorMessage = getWebGLErrorMessage;
+function getExtensionOrThrow(gl, extensionName) {
+    return throwIfNull(gl, function () { return gl.getExtension(extensionName); }, 'Extension "' + extensionName + '" not supported on this browser.');
+}
+exports.getExtensionOrThrow = getExtensionOrThrow;
+function createVertexShader(gl, vertexShaderSource) {
+    var vertexShader = throwIfNull(gl, function () { return gl.createShader(gl.VERTEX_SHADER); }, 'Unable to create vertex WebGLShader.');
+    callAndCheck(gl, function () { return gl.shaderSource(vertexShader, vertexShaderSource); });
+    callAndCheck(gl, function () { return gl.compileShader(vertexShader); });
+    if (gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS) === false) {
+        console.log(gl.getShaderInfoLog(vertexShader));
+        throw new Error('Failed to compile vertex shader.');
+    }
+    return vertexShader;
+}
+exports.createVertexShader = createVertexShader;
+function createFragmentShader(gl, fragmentShaderSource) {
+    var fragmentShader = throwIfNull(gl, function () { return gl.createShader(gl.FRAGMENT_SHADER); }, 'Unable to create fragment WebGLShader.');
+    callAndCheck(gl, function () { return gl.shaderSource(fragmentShader, fragmentShaderSource); });
+    callAndCheck(gl, function () { return gl.compileShader(fragmentShader); });
+    if (gl.getShaderParameter(fragmentShader, gl.COMPILE_STATUS) === false) {
+        logShaderSourceAndInfoLog(fragmentShaderSource, gl.getShaderInfoLog(fragmentShader));
+        throw new Error('Failed to compile fragment shader.');
+    }
+    return fragmentShader;
+}
+exports.createFragmentShader = createFragmentShader;
+var lineNumberRegex = /ERROR: [0-9]+:([0-9]+):/g;
+function logShaderSourceAndInfoLog(shaderSource, shaderInfoLog) {
+    var lineNumberRegexResult = lineNumberRegex.exec(shaderInfoLog);
+    if (lineNumberRegexResult == null) {
+        console.log("Couldn't parse line number in error: " + shaderInfoLog);
+        console.log(shaderSource);
+        return;
+    }
+    var lineNumber = +lineNumberRegexResult[1];
+    var shaderLines = shaderSource.split('\n');
+    var pad = shaderLines.length.toString().length + 2;
+    var linesWithLineNumbers = shaderLines.map(function (line, lineNumber) {
+        return util.rightPad((lineNumber + 1).toString(), pad) + line;
+    });
+    var maxLineLength = 0;
+    for (var i = 0; i < linesWithLineNumbers.length; i++) {
+        maxLineLength = Math.max(linesWithLineNumbers[i].length, maxLineLength);
+    }
+    var beforeErrorLines = linesWithLineNumbers.slice(0, lineNumber - 1);
+    var errorLine = linesWithLineNumbers.slice(lineNumber - 1, lineNumber);
+    var afterErrorLines = linesWithLineNumbers.slice(lineNumber);
+    console.log(beforeErrorLines.join('\n'));
+    console.log(shaderInfoLog.split('\n')[0]);
+    console.log("%c " + util.rightPad(errorLine[0], maxLineLength), 'border:1px solid red; background-color:#e3d2d2; color:#a61717');
+    console.log(afterErrorLines.join('\n'));
+}
+function createProgram(gl) {
+    return throwIfNull(gl, function () { return gl.createProgram(); }, 'Unable to create WebGLProgram.');
+}
+exports.createProgram = createProgram;
+function linkProgram(gl, program) {
+    callAndCheck(gl, function () { return gl.linkProgram(program); });
+    if (gl.getProgramParameter(program, gl.LINK_STATUS) === false) {
+        console.log(gl.getProgramInfoLog(program));
+        throw new Error('Failed to link vertex and fragment shaders.');
+    }
+}
+exports.linkProgram = linkProgram;
+function validateProgram(gl, program) {
+    callAndCheck(gl, function () { return gl.validateProgram(program); });
+    if (gl.getProgramParameter(program, gl.VALIDATE_STATUS) === false) {
+        console.log(gl.getProgramInfoLog(program));
+        throw new Error('Shader program validation failed.');
+    }
+}
+exports.validateProgram = validateProgram;
+function createStaticVertexBuffer(gl, data) {
+    var buffer = throwIfNull(gl, function () { return gl.createBuffer(); }, 'Unable to create WebGLBuffer');
+    callAndCheck(gl, function () { return gl.bindBuffer(gl.ARRAY_BUFFER, buffer); });
+    callAndCheck(gl, function () { return gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW); });
+    return buffer;
+}
+exports.createStaticVertexBuffer = createStaticVertexBuffer;
+function createStaticIndexBuffer(gl, data) {
+    var buffer = throwIfNull(gl, function () { return gl.createBuffer(); }, 'Unable to create WebGLBuffer');
+    callAndCheck(gl, function () { return gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, buffer); });
+    callAndCheck(gl, function () { return gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, data, gl.STATIC_DRAW); });
+    return buffer;
+}
+exports.createStaticIndexBuffer = createStaticIndexBuffer;
+function queryMaxTextureSize(gl) {
+    if (MAX_TEXTURE_SIZE != null) {
+        return MAX_TEXTURE_SIZE;
+    }
+    MAX_TEXTURE_SIZE =
+        callAndCheck(gl, function () { return gl.getParameter(gl.MAX_TEXTURE_SIZE); });
+    return MAX_TEXTURE_SIZE;
+}
+exports.queryMaxTextureSize = queryMaxTextureSize;
+function getChannelsPerTexture() {
+    if (!environment_1.ENV.get('WEBGL_FLOAT_TEXTURE_ENABLED')) {
+        return 4;
+    }
+    if (environment_1.ENV.get('WEBGL_VERSION') === 2) {
+        return 1;
+    }
+    return 4;
+}
+exports.getChannelsPerTexture = getChannelsPerTexture;
+function createTexture(gl) {
+    return throwIfNull(gl, function () { return gl.createTexture(); }, 'Unable to create WebGLTexture.');
+}
+exports.createTexture = createTexture;
+function validateTextureSize(gl, width, height) {
+    var maxTextureSize = queryMaxTextureSize(gl);
+    if ((width <= 0) || (height <= 0)) {
+        var requested = "[" + width + "x" + height + "]";
+        throw new Error('Requested texture size ' + requested + ' is invalid.');
+    }
+    if ((width > maxTextureSize) || (height > maxTextureSize)) {
+        var requested = "[" + width + "x" + height + "]";
+        var max = "[" + maxTextureSize + "x" + maxTextureSize + "]";
+        throw new Error('Requested texture size ' + requested +
+            ' greater than WebGL maximum on this browser / GPU ' + max + '.');
+    }
+}
+exports.validateTextureSize = validateTextureSize;
+function createFramebuffer(gl) {
+    return throwIfNull(gl, function () { return gl.createFramebuffer(); }, 'Unable to create WebGLFramebuffer.');
+}
+exports.createFramebuffer = createFramebuffer;
+function bindVertexBufferToProgramAttribute(gl, program, attribute, buffer, arrayEntriesPerItem, itemStrideInBytes, itemOffsetInBytes, attribLocations) {
+    var loc = -1;
+    if ((attribLocations != null) && (attribute in attribLocations)) {
+        loc = attribLocations[attribute];
+    }
+    else {
+        loc = gl.getAttribLocation(program, attribute);
+    }
+    if (loc === -1) {
+        return;
+    }
+    callAndCheck(gl, function () { return gl.bindBuffer(gl.ARRAY_BUFFER, buffer); });
+    callAndCheck(gl, function () { return gl.vertexAttribPointer(loc, arrayEntriesPerItem, gl.FLOAT, false, itemStrideInBytes, itemOffsetInBytes); });
+    callAndCheck(gl, function () { return gl.enableVertexAttribArray(loc); });
+}
+exports.bindVertexBufferToProgramAttribute = bindVertexBufferToProgramAttribute;
+function bindTextureUnit(gl, texture, textureUnit) {
+    validateTextureUnit(gl, textureUnit);
+    callAndCheck(gl, function () { return gl.activeTexture(gl.TEXTURE0 + textureUnit); });
+    callAndCheck(gl, function () { return gl.bindTexture(gl.TEXTURE_2D, texture); });
+}
+exports.bindTextureUnit = bindTextureUnit;
+function unbindTextureUnit(gl, textureUnit) {
+    validateTextureUnit(gl, textureUnit);
+    callAndCheck(gl, function () { return gl.activeTexture(gl.TEXTURE0 + textureUnit); });
+    callAndCheck(gl, function () { return gl.bindTexture(gl.TEXTURE_2D, null); });
+}
+exports.unbindTextureUnit = unbindTextureUnit;
+function getProgramUniformLocationOrThrow(gl, program, uniformName) {
+    return throwIfNull(gl, function () { return gl.getUniformLocation(program, uniformName); }, 'uniform "' + uniformName + '" not present in program.');
+}
+exports.getProgramUniformLocationOrThrow = getProgramUniformLocationOrThrow;
+function bindTextureToProgramUniformSampler(gl, program, texture, uniformSamplerLocation, textureUnit) {
+    callAndCheck(gl, function () { return bindTextureUnit(gl, texture, textureUnit); });
+    callAndCheck(gl, function () { return gl.uniform1i(uniformSamplerLocation, textureUnit); });
+}
+exports.bindTextureToProgramUniformSampler = bindTextureToProgramUniformSampler;
+function bindCanvasToFramebuffer(gl) {
+    callAndCheck(gl, function () { return gl.bindFramebuffer(gl.FRAMEBUFFER, null); });
+    callAndCheck(gl, function () { return gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); });
+    callAndCheck(gl, function () { return gl.scissor(0, 0, gl.canvas.width, gl.canvas.height); });
+}
+exports.bindCanvasToFramebuffer = bindCanvasToFramebuffer;
+function bindColorTextureToFramebuffer(gl, texture, framebuffer) {
+    callAndCheck(gl, function () { return gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); });
+    callAndCheck(gl, function () { return gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); });
+}
+exports.bindColorTextureToFramebuffer = bindColorTextureToFramebuffer;
+function unbindColorTextureFromFramebuffer(gl, framebuffer) {
+    callAndCheck(gl, function () { return gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); });
+    callAndCheck(gl, function () { return gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0); });
+}
+exports.unbindColorTextureFromFramebuffer = unbindColorTextureFromFramebuffer;
+function validateFramebuffer(gl) {
+    var status = gl.checkFramebufferStatus(gl.FRAMEBUFFER);
+    if (status !== gl.FRAMEBUFFER_COMPLETE) {
+        throw new Error('Error binding framebuffer: ' + getFramebufferErrorMessage(gl, status));
+    }
+}
+exports.validateFramebuffer = validateFramebuffer;
+function getFramebufferErrorMessage(gl, status) {
+    switch (status) {
+        case gl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
+            return 'FRAMEBUFFER_INCOMPLETE_ATTACHMENT';
+        case gl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
+            return 'FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT';
+        case gl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
+            return 'FRAMEBUFFER_INCOMPLETE_DIMENSIONS';
+        case gl.FRAMEBUFFER_UNSUPPORTED:
+            return 'FRAMEBUFFER_UNSUPPORTED';
+        default:
+            return "unknown error " + status;
+    }
+}
+exports.getFramebufferErrorMessage = getFramebufferErrorMessage;
+function throwIfNull(gl, returnTOrNull, failureMessage) {
+    var tOrNull = callAndCheck(gl, function () { return returnTOrNull(); });
+    if (tOrNull == null) {
+        throw new Error(failureMessage);
+    }
+    return tOrNull;
+}
+function validateTextureUnit(gl, textureUnit) {
+    var maxTextureUnit = gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS - 1;
+    var glTextureUnit = textureUnit + gl.TEXTURE0;
+    if (glTextureUnit < gl.TEXTURE0 || glTextureUnit > maxTextureUnit) {
+        var textureUnitRange = "[gl.TEXTURE0, gl.TEXTURE" + maxTextureUnit + "]";
+        throw new Error("textureUnit must be in " + textureUnitRange + ".");
+    }
+}
+function getTextureShapeFromLogicalShape(gl, logShape) {
+    if (logShape.length !== 2) {
+        var squeezeResult = util.squeezeShape(logShape);
+        logShape = squeezeResult.newShape;
+    }
+    var maxTexSize = queryMaxTextureSize(gl);
+    var size = util.sizeFromShape(logShape);
+    if (logShape.length <= 1 && size <= maxTexSize) {
+        return [size, 1];
+    }
+    else if (logShape.length === 2 && logShape[0] <= maxTexSize &&
+        logShape[1] <= maxTexSize) {
+        return logShape;
+    }
+    else if (logShape.length === 3 && logShape[0] <= maxTexSize &&
+        logShape[1] * logShape[2] <= maxTexSize) {
+        return [logShape[0], logShape[1] * logShape[2]];
+    }
+    else if (logShape.length === 4 && logShape[0] <= maxTexSize &&
+        logShape[1] * logShape[2] * logShape[3] <= maxTexSize) {
+        return [logShape[0], logShape[1] * logShape[2] * logShape[3]];
+    }
+    else {
+        return util.sizeToSquarishShape(size);
+    }
+}
+exports.getTextureShapeFromLogicalShape = getTextureShapeFromLogicalShape;
+
+},{"../../../environment":15,"../../../util":101}],90:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+function getBroadcastDims(inShape, outShape) {
+    var inRank = inShape.length;
+    var dims = [];
+    for (var i = 0; i < inRank; i++) {
+        var dim = inRank - 1 - i;
+        var a = inShape[dim] || 1;
+        var b = outShape[outShape.length - 1 - i] || 1;
+        if (b > 1 && a === 1) {
+            dims.unshift(dim);
+        }
+    }
+    return dims;
+}
+exports.getBroadcastDims = getBroadcastDims;
+function broadcastDimsAreOuter(dims) {
+    for (var i = 0; i < dims.length; i++) {
+        if (dims[i] !== i) {
+            return false;
+        }
+    }
+    return true;
+}
+exports.broadcastDimsAreOuter = broadcastDimsAreOuter;
+function assertAndGetBroadcastShape(shapeA, shapeB) {
+    var result = [];
+    var errMsg = "Operands could not be broadcast together with shapes " +
+        (shapeA + " and " + shapeB + ".");
+    var l = Math.max(shapeA.length, shapeB.length);
+    for (var i = 0; i < l; i++) {
+        var a = shapeA[shapeA.length - i - 1] || 1;
+        var b = shapeB[shapeB.length - i - 1] || 1;
+        if (a > 1 && b > 1 && a !== b) {
+            throw Error(errMsg);
+        }
+        result.unshift(Math.max(a, b));
+    }
+    return result;
+}
+exports.assertAndGetBroadcastShape = assertAndGetBroadcastShape;
+
+},{}],91:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../util");
+function assertParams(aShape, bShape, axis) {
+    var aRank = aShape.length;
+    var bRank = bShape.length;
+    util.assert(aShape.length === bShape.length, "Error in concat" + aRank + "D: rank of x1 (" + aRank + ") and x2 (" + bRank + ") " +
+        "must be the same.");
+    util.assert(axis >= 0 && axis < aRank, "Error in concat" + aRank + "D: axis must be " +
+        ("between 0 and " + (aRank - 1) + "."));
+    for (var i = 0; i < aRank; i++) {
+        util.assert((i === axis) || (aShape[i] === bShape[i]), "Error in concat" + aRank + "D: Shape (" + aShape + ") does not match " +
+            ("(" + bShape + ") along the non-concatenated axis " + i + "."));
+    }
+}
+exports.assertParams = assertParams;
+function computeOutShape(x1Shape, x2Shape, axis) {
+    util.assert(x1Shape.length === x2Shape.length, 'x1 and x2 should have the same rank.');
+    var outputShape = x1Shape.slice();
+    outputShape[axis] += x2Shape[axis];
+    return outputShape;
+}
+exports.computeOutShape = computeOutShape;
+
+},{"../util":101}],92:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../util");
+function computePool2DInfo(inShape, filterSize, strides, pad, dataFormat) {
+    if (dataFormat === void 0) { dataFormat = 'channelsLast'; }
+    var _a = parseTupleParam(filterSize), filterHeight = _a[0], filterWidth = _a[1];
+    var filterShape;
+    if (dataFormat === 'channelsLast') {
+        filterShape = [filterHeight, filterWidth, inShape[3], inShape[3]];
+    }
+    else if (dataFormat === 'channelsFirst') {
+        filterShape = [filterHeight, filterWidth, inShape[1], inShape[1]];
+    }
+    else {
+        throw new Error("Unknown dataFormat " + dataFormat);
+    }
+    return computeConv2DInfo(inShape, filterShape, strides, pad, false, dataFormat);
+}
+exports.computePool2DInfo = computePool2DInfo;
+function computeConv2DInfo(inShape, filterShape, strides, pad, depthwise, dataFormat) {
+    if (depthwise === void 0) { depthwise = false; }
+    if (dataFormat === void 0) { dataFormat = 'channelsLast'; }
+    var _a = [-1, -1, -1, -1], batchSize = _a[0], inHeight = _a[1], inWidth = _a[2], inChannels = _a[3];
+    if (dataFormat === 'channelsLast') {
+        batchSize = inShape[0], inHeight = inShape[1], inWidth = inShape[2], inChannels = inShape[3];
+    }
+    else if (dataFormat === 'channelsFirst') {
+        batchSize = inShape[0], inChannels = inShape[1], inHeight = inShape[2], inWidth = inShape[3];
+    }
+    else {
+        throw new Error("Unknown dataFormat " + dataFormat);
+    }
+    var filterHeight = filterShape[0], filterWidth = filterShape[1], filterChannels = filterShape[3];
+    var _b = parseTupleParam(strides), strideHeight = _b[0], strideWidth = _b[1];
+    var _c = getPadAndOutInfo(pad, inHeight, inWidth, strideHeight, strideWidth, filterHeight, filterWidth), padInfo = _c.padInfo, outHeight = _c.outHeight, outWidth = _c.outWidth;
+    var outChannels = depthwise ? filterChannels * inChannels : filterChannels;
+    var outShape;
+    if (dataFormat === 'channelsFirst') {
+        outShape = [batchSize, outChannels, outHeight, outWidth];
+    }
+    else if (dataFormat === 'channelsLast') {
+        outShape = [batchSize, outHeight, outWidth, outChannels];
+    }
+    return {
+        batchSize: batchSize,
+        dataFormat: dataFormat,
+        inHeight: inHeight,
+        inWidth: inWidth,
+        inChannels: inChannels,
+        outHeight: outHeight,
+        outWidth: outWidth,
+        outChannels: outChannels,
+        padInfo: padInfo,
+        strideHeight: strideHeight,
+        strideWidth: strideWidth,
+        filterHeight: filterHeight,
+        filterWidth: filterWidth,
+        inShape: inShape,
+        outShape: outShape,
+        filterShape: filterShape
+    };
+}
+exports.computeConv2DInfo = computeConv2DInfo;
+function computeOutputShape3D(inShape, fieldSize, outDepth, stride, zeroPad) {
+    if (zeroPad == null) {
+        zeroPad = computeDefaultPad(inShape, fieldSize, stride);
+    }
+    var inputRows = inShape[0];
+    var inputCols = inShape[1];
+    var outputRows = (inputRows - fieldSize + 2 * zeroPad) / stride + 1;
+    util.assert(util.isInt(outputRows), "The output # of rows (" + outputRows + ") must be an integer. Change the " +
+        "stride and/or zero pad parameters");
+    var outputCols = (inputCols - fieldSize + 2 * zeroPad) / stride + 1;
+    util.assert(util.isInt(outputCols), "The output # of columns (" + outputCols + ") must be an integer. Change " +
+        "the stride and/or zero pad parameters");
+    return [outputRows, outputCols, outDepth];
+}
+exports.computeOutputShape3D = computeOutputShape3D;
+function computeDefaultPad(inputShape, fieldSize, stride) {
+    return Math.floor((inputShape[0] * (stride - 1) - stride + fieldSize) / 2);
+}
+exports.computeDefaultPad = computeDefaultPad;
+function computeWeightsShape4D(inputDepth, outputDepth, filterHeight, filterWidth) {
+    return [filterHeight, filterWidth, inputDepth, outputDepth];
+}
+exports.computeWeightsShape4D = computeWeightsShape4D;
+function computeDilatedRC(rc, origStride) {
+    var rowsDilated = (rc[0] - 1) * origStride + 1;
+    var colsDilated = (rc[1] - 1) * origStride + 1;
+    return [rowsDilated, colsDilated];
+}
+exports.computeDilatedRC = computeDilatedRC;
+function parseTupleParam(param) {
+    return typeof param === 'number' ? [param, param] : param;
+}
+function getPadAndOutInfo(pad, inHeight, inWidth, strideHeight, strideWidth, filterHeight, filterWidth) {
+    var padInfo;
+    var outHeight;
+    var outWidth;
+    if (typeof pad === 'number') {
+        padInfo = { top: pad, bottom: pad, left: pad, right: pad };
+        var outShape = computeOutputShape3D([inHeight, inWidth, 1], filterHeight, 1, strideHeight, pad);
+        outHeight = outShape[0];
+        outWidth = outShape[1];
+    }
+    else if (pad === 'same') {
+        outHeight = Math.ceil(inHeight / strideHeight);
+        outWidth = Math.ceil(inWidth / strideWidth);
+        var padAlongHeight = (outHeight - 1) * strideHeight + filterHeight - inHeight;
+        var padAlongWidth = (outWidth - 1) * strideWidth + filterWidth - inWidth;
+        var top_1 = Math.floor(padAlongHeight / 2);
+        var bottom = padAlongHeight - top_1;
+        var left = Math.floor(padAlongWidth / 2);
+        var right = padAlongWidth - left;
+        padInfo = { top: top_1, bottom: bottom, left: left, right: right };
+    }
+    else if (pad === 'valid') {
+        padInfo = { top: 0, bottom: 0, left: 0, right: 0 };
+        outHeight = Math.ceil((inHeight - filterHeight + 1) / strideHeight);
+        outWidth = Math.ceil((inWidth - filterWidth + 1) / strideWidth);
+    }
+    else {
+        throw Error("Unknown padding parameter: " + pad);
+    }
+    return { padInfo: padInfo, outHeight: outHeight, outWidth: outWidth };
+}
+
+},{"../util":101}],93:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../environment");
+var ndarray_1 = require("./ndarray");
+var SquareCostFunc = (function () {
+    function SquareCostFunc() {
+        this.halfOne = environment_1.ENV.math.keep(ndarray_1.Scalar.new(0.5));
+    }
+    SquareCostFunc.prototype.cost = function (math, x1, x2) {
+        var diff = math.subStrict(x1, x2);
+        var diffSquared = math.elementWiseMul(diff, diff);
+        var result = math.scalarTimesArray(this.halfOne, diffSquared);
+        diff.dispose();
+        diffSquared.dispose();
+        return result;
+    };
+    SquareCostFunc.prototype.der = function (math, x1, x2) {
+        return math.subStrict(x1, x2);
+    };
+    SquareCostFunc.prototype.dispose = function () {
+        this.halfOne.dispose();
+    };
+    return SquareCostFunc;
+}());
+exports.SquareCostFunc = SquareCostFunc;
+
+},{"../environment":15,"./ndarray":95}],94:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../environment");
+var util = require("../util");
+var axis_util = require("./axis_util");
+var backend_engine_1 = require("./backends/backend_engine");
+var matmul_1 = require("./backends/types/matmul");
+var broadcast_util = require("./broadcast_util");
+var concat_util = require("./concat_util");
+var conv_util = require("./conv_util");
+var ndarray_1 = require("./ndarray");
+var slice_util = require("./slice_util");
+var NDArrayMath = (function () {
+    function NDArrayMath(backend, safeMode) {
+        this.safeMode = safeMode;
+        this.numArrays = 0;
+        this.customBackend = false;
+        this.ndarrayScopes = [];
+        this.ndarraysToKeep = [];
+        this.activeScopeNDArraysToKeep = [];
+        if (typeof backend === 'string') {
+            this.backend = environment_1.ENV.getBackend(backend);
+        }
+        else {
+            this.customBackend = true;
+            this.backend = backend;
+        }
+        this.backendEngine = new backend_engine_1.BackendEngine(this.backend);
+    }
+    NDArrayMath.prototype.time = function (query) {
+        return this.backend.time(query);
+    };
+    NDArrayMath.prototype.getNumArrays = function () {
+        return this.numArrays;
+    };
+    NDArrayMath.prototype.register = function (a) {
+        this.track(a);
+        this.numArrays++;
+    };
+    NDArrayMath.prototype.writePixels = function (id, pixels, numChannels) {
+        this.backend.writePixels(id, pixels, numChannels);
+    };
+    NDArrayMath.prototype.write = function (id, values, dtype, shape) {
+        this.backend.write(id, values, dtype, shape);
+    };
+    NDArrayMath.prototype.readSync = function (id) {
+        return this.backend.readSync(id);
+    };
+    NDArrayMath.prototype.read = function (id) {
+        return this.backend.read(id);
+    };
+    NDArrayMath.prototype.scope = function (scopeFn) {
+        var _this = this;
+        this.startScope();
+        var keepFn = function (ndarray) { return _this.keep(ndarray); };
+        var trackFn = function (ndarray) { return ndarray; };
+        var result = scopeFn(keepFn, trackFn);
+        if (result instanceof Promise) {
+            result.then(function (r) { return _this.endScope(r); });
+            return result;
+        }
+        else {
+            this.endScope(result);
+            return result;
+        }
+    };
+    NDArrayMath.prototype.enableDebugMode = function () {
+        this.backendEngine.enableDebugMode();
+        console.warn('Debugging mode is ON. The output of every math call will ' +
+            'be downloaded to CPU and checked for NaNs. ' +
+            'This significantly impacts performance.');
+    };
+    NDArrayMath.prototype.startScope = function () {
+        var newScope = [];
+        this.ndarrayScopes.push(newScope);
+        this.activeScope = newScope;
+        var newNDArraysToKeep = [];
+        this.ndarraysToKeep.push(newNDArraysToKeep);
+        this.activeScopeNDArraysToKeep = newNDArraysToKeep;
+    };
+    NDArrayMath.prototype.extractNDArraysFromScopeResult = function (result) {
+        if (result == null) {
+            return [];
+        }
+        if (result instanceof ndarray_1.NDArray) {
+            return [result];
+        }
+        var list = [];
+        var resultObj = result;
+        for (var k in resultObj) {
+            var val = resultObj[k];
+            if (val instanceof ndarray_1.NDArray) {
+                list.push(val);
+            }
+        }
+        return list;
+    };
+    NDArrayMath.prototype.endScope = function (result) {
+        var _this = this;
+        var arraysToKeep = this.activeScopeNDArraysToKeep;
+        var resultArrays = this.extractNDArraysFromScopeResult(result);
+        arraysToKeep = arraysToKeep.concat(resultArrays);
+        for (var i = 0; i < this.activeScope.length; i++) {
+            var ndarray = this.activeScope[i];
+            if (this.isNDArrayDataInList(ndarray, arraysToKeep)) {
+                continue;
+            }
+            ndarray.dispose();
+        }
+        this.ndarrayScopes.pop();
+        this.activeScope = this.ndarrayScopes.length === 0 ?
+            null :
+            this.ndarrayScopes[this.ndarrayScopes.length - 1];
+        resultArrays.forEach(function (val) {
+            if (!_this.isNDArrayDataInList(val, _this.activeScopeNDArraysToKeep)) {
+                _this.track(val);
+            }
+        });
+        this.ndarraysToKeep.pop();
+        this.activeScopeNDArraysToKeep = this.ndarraysToKeep.length === 0 ?
+            null :
+            this.ndarraysToKeep[this.ndarraysToKeep.length - 1];
+    };
+    NDArrayMath.prototype.isNDArrayDataInList = function (ndarray, ndarrayList) {
+        for (var i = 0; i < ndarrayList.length; i++) {
+            if (ndarrayList[i].id === ndarray.id) {
+                return true;
+            }
+        }
+        return false;
+    };
+    NDArrayMath.prototype.keep = function (result) {
+        if (this.activeScope == null) {
+            if (this.safeMode) {
+                throw new Error('You are using math in safe mode. Enclose all ' +
+                    'math.method() calls inside a scope: ' +
+                    'math.scope(() => {math.method();...}) to avoid memory ' +
+                    'leaks.');
+            }
+            return result;
+        }
+        this.activeScopeNDArraysToKeep.push(result);
+        return result;
+    };
+    NDArrayMath.prototype.track = function (result) {
+        if (this.activeScope == null) {
+            if (this.safeMode) {
+                throw new Error('You are using math in safe mode. Enclose all ' +
+                    'math.method() calls inside a scope: ' +
+                    'math.scope(() => {math.method();...}) to avoid memory ' +
+                    'leaks.');
+            }
+            return result;
+        }
+        this.activeScope.push(result);
+        return result;
+    };
+    NDArrayMath.prototype.dispose = function () {
+        if (this.customBackend) {
+            this.backend.dispose();
+        }
+    };
+    NDArrayMath.prototype.matMul = function (a, b, aOrientation, bOrientation) {
+        var _this = this;
+        if (aOrientation === void 0) { aOrientation = matmul_1.MatrixOrientation.REGULAR; }
+        if (bOrientation === void 0) { bOrientation = matmul_1.MatrixOrientation.REGULAR; }
+        var innerShapeA = (aOrientation === matmul_1.MatrixOrientation.REGULAR) ? a.shape[1] : a.shape[0];
+        var innerShapeB = (bOrientation === matmul_1.MatrixOrientation.REGULAR) ? b.shape[0] : b.shape[1];
+        util.assert(a.rank === 2 && b.rank === 2, "Error in matMul: inputs must be rank 2, got ranks " + a.rank +
+            (" and " + b.rank + "."));
+        util.assert(innerShapeA === innerShapeB, "Error in matMul: inner shapes (" + innerShapeA + ") and (" +
+            (innerShapeB + ") of NDArrays with shapes " + a.shape + " and ") +
+            (b.shape + " and orientations " + matmul_1.MatrixOrientation[aOrientation]) +
+            (" and " + matmul_1.MatrixOrientation[bOrientation] + " must match."));
+        return this.backendEngine.executeKernel('MatMul', { inputs: { a: a, b: b }, args: { aOrientation: aOrientation, bOrientation: bOrientation } }, function (dy, y) {
+            return {
+                a: function () { return _this.matMul(dy, b, matmul_1.MatrixOrientation.REGULAR, matmul_1.MatrixOrientation.TRANSPOSED); },
+                b: function () { return _this.matMul(a, dy, matmul_1.MatrixOrientation.TRANSPOSED, matmul_1.MatrixOrientation.REGULAR); }
+            };
+        });
+    };
+    NDArrayMath.prototype.executeOp = function (name, f) {
+        return f();
+    };
+    NDArrayMath.prototype.vectorTimesMatrix = function (v, matrix) {
+        util.assert(v.rank === 1, "Error in vectorTimesMatrix: first input must be rank 1, but got " +
+            ("rank " + v.rank + "."));
+        util.assert(matrix.rank === 2, "Error in vectorTimesMatrix: second input must be rank 2, but got " +
+            ("rank " + matrix.rank + "."));
+        util.assert(v.size === matrix.shape[0], "Error in vectorTimesMatrix: size of vector (" + v.size + ") " +
+            ("must match first dimension of matrix (" + matrix.shape[0] + ")"));
+        return this.matMul(v.as2D(1, -1), matrix).as1D();
+    };
+    NDArrayMath.prototype.matrixTimesVector = function (matrix, v) {
+        util.assert(v.rank === 1, "Error in matrixTimesVector: second input must rank 1, but got " +
+            ("rank " + v.rank + "."));
+        util.assert(matrix.rank === 2, "Error in matrixTimesVector: first input must be a rank 2, but got " +
+            ("rank " + matrix.rank + "."));
+        util.assert(v.size === matrix.shape[1], "Error in matrixTimesVector: size of first rank 1 input " + v.size + " " +
+            "must match inner dimension of second rank 2 input, but got " +
+            ("shape " + matrix.shape + "."));
+        return this.matMul(matrix, v.as2D(-1, 1)).as1D();
+    };
+    NDArrayMath.prototype.dotProduct = function (v1, v2) {
+        util.assert(v1.rank === 1 && v2.rank === 1, "Error in dotProduct: inputs must be rank 1, but got ranks " +
+            (v1.rank + " and " + v2.rank + "."));
+        util.assert(v1.size === v2.size, "Error in dotProduct: size of inputs (" + v1.size + ") and (" +
+            (v2.size + ") must match."));
+        return this.matMul(v1.as2D(1, -1), v2.as2D(-1, 1)).asScalar();
+    };
+    NDArrayMath.prototype.outerProduct = function (v1, v2) {
+        util.assert(v1.rank === 1 && v2.rank === 1, "Error in outerProduct: inputs must be rank 1, but got ranks " +
+            (v1.rank + " and " + v2.rank + "."));
+        return this.matMul(v1.as2D(-1, 1), v2.as2D(1, -1));
+    };
+    NDArrayMath.prototype.clone = function (x) {
+        return this.backendEngine.executeKernel('Clone', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.reshape = function (ndarray, newShape) {
+        console.warn('math.reshape() is deprecated. Please call reshape() ' +
+            'directly on the ndarray object');
+        return ndarray.reshape(newShape);
+    };
+    NDArrayMath.prototype.slice1D = function (x, begin, size) {
+        slice_util.assertParamsValid(x, [begin], [size]);
+        return this.backendEngine.executeKernel('Slice1D', { inputs: { x: x }, args: { begin: begin, size: size } });
+    };
+    NDArrayMath.prototype.slice2D = function (x, begin, size) {
+        slice_util.assertParamsValid(x, begin, size);
+        return this.backendEngine.executeKernel('Slice2D', { inputs: { x: x }, args: { begin: begin, size: size } });
+    };
+    NDArrayMath.prototype.slice3D = function (x, begin, size) {
+        slice_util.assertParamsValid(x, begin, size);
+        return this.backendEngine.executeKernel('Slice3D', { inputs: { x: x }, args: { begin: begin, size: size } });
+    };
+    NDArrayMath.prototype.slice4D = function (x, begin, size) {
+        slice_util.assertParamsValid(x, begin, size);
+        return this.backendEngine.executeKernel('Slice4D', { inputs: { x: x }, args: { begin: begin, size: size } });
+    };
+    NDArrayMath.prototype.concat1D = function (a, b) {
+        concat_util.assertParams(a.shape, b.shape, 0);
+        return this.backendEngine.executeKernel('Concat1D', { inputs: { a: a, b: b } });
+    };
+    NDArrayMath.prototype.concat2D = function (a, b, axis) {
+        concat_util.assertParams(a.shape, b.shape, axis);
+        return this.backendEngine.executeKernel('Concat2D', { inputs: { a: a, b: b }, args: { axis: axis } });
+    };
+    NDArrayMath.prototype.concat3D = function (a, b, axis) {
+        concat_util.assertParams(a.shape, b.shape, axis);
+        return this.backendEngine.executeKernel('Concat3D', { inputs: { a: a, b: b }, args: { axis: axis } });
+    };
+    NDArrayMath.prototype.concat4D = function (a, b, axis) {
+        concat_util.assertParams(a.shape, b.shape, axis);
+        return this.backendEngine.executeKernel('Concat4D', { inputs: { a: a, b: b }, args: { axis: axis } });
+    };
+    NDArrayMath.prototype.logSumExp = function (input, axis, keepDims) {
+        var _this = this;
+        if (axis === void 0) { axis = null; }
+        if (keepDims === void 0) { keepDims = false; }
+        var axes = axis_util.parseAxisParam(axis, input.shape);
+        return this.executeOp('logSumExp', function () {
+            var xMax = _this.max(input, axes, true);
+            var a = _this.subtract(input, xMax);
+            var b = _this.exp(a);
+            var c = _this.sum(b, axes);
+            var d = _this.log(c);
+            var res = _this.add(xMax.reshape(d.shape), d);
+            if (keepDims) {
+                var newShape = axis_util.expandShapeToKeepDim(res.shape, axes);
+                return res.reshape(newShape);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.sum = function (x, axis, keepDims) {
+        var _this = this;
+        if (axis === void 0) { axis = null; }
+        if (keepDims === void 0) { keepDims = false; }
+        var origAxes = axis_util.parseAxisParam(axis, x.shape);
+        var axes = origAxes;
+        var permutedAxes = axis_util.getPermutedAxes(axes, x.rank);
+        return this.executeOp('sum', function () {
+            if (permutedAxes != null) {
+                x = _this.transpose(x, permutedAxes);
+                axes = axis_util.getInnerMostAxes(axes.length, x.rank);
+            }
+            var res = _this.backendEngine.executeKernel('Sum', { inputs: { x: x }, args: { axes: axes } }, function (dy, y) {
+                return {
+                    x: function () {
+                        if (axis != null) {
+                            throw new Error("Gradients for sum with axis reduction not yet " +
+                                "supported.");
+                        }
+                        return _this.multiply(dy, ndarray_1.NDArray.onesLike(x));
+                    }
+                };
+            });
+            if (keepDims) {
+                var newShape = axis_util.expandShapeToKeepDim(res.shape, origAxes);
+                return res.reshape(newShape);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.mean = function (x, axis, keepDims) {
+        var _this = this;
+        if (axis === void 0) { axis = null; }
+        if (keepDims === void 0) { keepDims = false; }
+        var axes = axis_util.parseAxisParam(axis, x.shape);
+        var shapes = axis_util.computeOutAndReduceShapes(x.shape, axes);
+        var reduceShape = shapes[1];
+        var reduceSize = util.sizeFromShape(reduceShape);
+        return this.executeOp('mean', function () {
+            return _this.scope(function (keep) {
+                var res = _this.divide(x, ndarray_1.Scalar.new(reduceSize));
+                return _this.sum(res, axis, keepDims);
+            });
+        });
+    };
+    NDArrayMath.prototype.argMin = function (x, axis) {
+        var _this = this;
+        if (axis === void 0) { axis = null; }
+        var axes = axis_util.parseAxisParam(axis, x.shape);
+        var permutedAxes = axis_util.getPermutedAxes(axes, x.rank);
+        return this.executeOp('argMin', function () {
+            if (permutedAxes != null) {
+                x = _this.transpose(x, permutedAxes);
+                axes = axis_util.getInnerMostAxes(axes.length, x.rank);
+            }
+            return _this.backendEngine.executeKernel('ArgMin', { inputs: { x: x }, args: { axes: axes } });
+        });
+    };
+    NDArrayMath.prototype.argMax = function (x, axis) {
+        var _this = this;
+        if (axis === void 0) { axis = null; }
+        var axes = axis_util.parseAxisParam(axis, x.shape);
+        var permutedAxes = axis_util.getPermutedAxes(axes, x.rank);
+        return this.executeOp('argMax', function () {
+            if (permutedAxes != null) {
+                x = _this.transpose(x, permutedAxes);
+                axes = axis_util.getInnerMostAxes(axes.length, x.rank);
+            }
+            return _this.backendEngine.executeKernel('ArgMax', { inputs: { x: x }, args: { axes: axes } });
+        });
+    };
+    NDArrayMath.prototype.argMaxEquals = function (x1, x2) {
+        var _this = this;
+        util.assertShapesMatch(x1.shape, x2.shape, 'Error in argMaxEquals: ');
+        return this.executeOp('argMaxEquals', function () { return _this.scope(function () {
+            return _this.equal(_this.argMax(x1), _this.argMax(x2));
+        }); });
+    };
+    NDArrayMath.prototype.equal = function (a, b) {
+        return this.backendEngine.executeKernel('Equal', { inputs: { a: a, b: b } });
+    };
+    NDArrayMath.prototype.equalStrict = function (a, b) {
+        util.assertShapesMatch(a.shape, b.shape, 'Error in equalStrict: ');
+        return this.equal(a, b);
+    };
+    NDArrayMath.prototype.topK = function (x, k) {
+        var _this = this;
+        util.assert(k <= x.size, "Error in topK: k value (" + k + ") must be less than size of input " +
+            ("ndarray, got shape " + x.shape + "."));
+        var values;
+        var indices;
+        this.executeOp('topK', function () {
+            values = _this.backendEngine.executeKernel('TopKValues', { inputs: { x: x }, args: { k: k } });
+            indices = _this.backendEngine.executeKernel('TopKIndices', { inputs: { x: x }, args: { k: k } });
+            return values;
+        });
+        var result = { values: values, indices: indices };
+        return result;
+    };
+    NDArrayMath.prototype.min = function (x, axis, keepDims) {
+        var _this = this;
+        if (axis === void 0) { axis = null; }
+        if (keepDims === void 0) { keepDims = false; }
+        var origAxes = axis_util.parseAxisParam(axis, x.shape);
+        var axes = origAxes;
+        var permutedAxes = axis_util.getPermutedAxes(axes, x.rank);
+        return this.executeOp('min', function () {
+            if (permutedAxes != null) {
+                x = _this.transpose(x, permutedAxes);
+                axes = axis_util.getInnerMostAxes(axes.length, x.rank);
+            }
+            var res = _this.backendEngine.executeKernel('Min', { inputs: { x: x }, args: { axes: axes } });
+            if (keepDims) {
+                var newShape = axis_util.expandShapeToKeepDim(res.shape, origAxes);
+                return res.reshape(newShape);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.max = function (x, axis, keepDims) {
+        var _this = this;
+        if (axis === void 0) { axis = null; }
+        if (keepDims === void 0) { keepDims = false; }
+        var origAxes = axis_util.parseAxisParam(axis, x.shape);
+        var axes = origAxes;
+        var permutedAxes = axis_util.getPermutedAxes(axes, x.rank);
+        return this.executeOp('max', function () {
+            if (permutedAxes != null) {
+                x = _this.transpose(x, permutedAxes);
+                axes = axis_util.getInnerMostAxes(axes.length, x.rank);
+            }
+            var res = _this.backendEngine.executeKernel('Max', { inputs: { x: x }, args: { axes: axes } });
+            if (keepDims) {
+                var newShape = axis_util.expandShapeToKeepDim(res.shape, origAxes);
+                return res.reshape(newShape);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.softmax = function (logits, dim) {
+        var _this = this;
+        if (dim === void 0) { dim = -1; }
+        if (dim === -1) {
+            dim = logits.rank - 1;
+        }
+        if (dim !== logits.rank - 1) {
+            throw Error('Softmax along a non-last dimension is not yet supported. ' +
+                ("Logits was rank " + logits.rank + " and dim was " + dim));
+        }
+        return this.executeOp('softmax', function () {
+            return _this.scope(function () {
+                var lse = _this.logSumExp(logits, [dim], true);
+                var logResult = _this.subtract(logits, lse);
+                return _this.exp(logResult);
+            });
+        });
+    };
+    NDArrayMath.prototype.switchDim = function (a, newDim) {
+        return this.transpose(a, newDim);
+    };
+    NDArrayMath.prototype.tile = function (x, reps) {
+        util.assert(x.rank === reps.length, "Error in transpose: rank of input " + x.rank + " " +
+            ("must match length of reps " + reps + "."));
+        return this.backendEngine.executeKernel('Tile', { inputs: { x: x }, args: { reps: reps } });
+    };
+    NDArrayMath.prototype.transpose = function (x, perm) {
+        if (perm == null) {
+            perm = x.shape.map(function (s, i) { return i; }).reverse();
+        }
+        util.assert(x.rank === perm.length, "Error in transpose: rank of input " + x.rank + " " +
+            ("must match length of perm " + perm + "."));
+        return this.backendEngine.executeKernel('Transpose', { inputs: { x: x }, args: { perm: perm } });
+    };
+    NDArrayMath.prototype.scalarPlusArray = function (c, a) {
+        util.assert(c.size === 1, "Error in scalarPlusArray: first argument must be rank 0, but got " +
+            ("rank " + c.rank + "."));
+        return this.add(c, a);
+    };
+    NDArrayMath.prototype.scalarMinusArray = function (c, a) {
+        util.assert(c.size === 1, "Error in scalarMinusArray: first argument must be rank 0, but got " +
+            ("rank " + c.rank + "."));
+        return this.subtract(c, a);
+    };
+    NDArrayMath.prototype.arrayMinusScalar = function (a, c) {
+        util.assert(c.size === 1, "Error in arrayMinusScalar: second argument must be rank 0, but " +
+            ("got rank " + c.rank + "."));
+        return this.subtract(a, c);
+    };
+    NDArrayMath.prototype.neg = function (x) {
+        return this.backendEngine.executeKernel('Neg', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.add = function (a, b) {
+        broadcast_util.assertAndGetBroadcastShape(a.shape, b.shape);
+        return this.backendEngine.executeKernel('Add', { inputs: { a: a, b: b } });
+    };
+    NDArrayMath.prototype.addStrict = function (a, b) {
+        util.assertShapesMatch(a.shape, b.shape, 'Error in addStrict: ');
+        return this.add(a, b);
+    };
+    NDArrayMath.prototype.subtract = function (a, b) {
+        broadcast_util.assertAndGetBroadcastShape(a.shape, b.shape);
+        return this.backendEngine.executeKernel('Sub', { inputs: { a: a, b: b } });
+    };
+    NDArrayMath.prototype.pow = function (a, b) {
+        util.assert(b.dtype === 'int32', 'only supports int32 data type for the exponent parameter.');
+        broadcast_util.assertAndGetBroadcastShape(a.shape, b.shape);
+        return this.backendEngine.executeKernel('Pow', { inputs: { a: a, b: b } });
+    };
+    NDArrayMath.prototype.powStrict = function (a, b) {
+        util.assertShapesMatch(a.shape, b.shape, 'Error in powStrict: ');
+        return this.pow(a, b);
+    };
+    NDArrayMath.prototype.sub = function (a, b) {
+        return this.subtract(a, b);
+    };
+    NDArrayMath.prototype.subStrict = function (a, b) {
+        util.assertShapesMatch(a.shape, b.shape, 'Error in subStrict: ');
+        return this.subtract(a, b);
+    };
+    NDArrayMath.prototype.multiply = function (a, b) {
+        broadcast_util.assertAndGetBroadcastShape(a.shape, b.shape);
+        return this.backendEngine.executeKernel('Mul', { inputs: { a: a, b: b } });
+    };
+    NDArrayMath.prototype.elementWiseMul = function (a, b) {
+        return this.multiplyStrict(a, b);
+    };
+    NDArrayMath.prototype.multiplyStrict = function (a, b) {
+        util.assertShapesMatch(a.shape, b.shape, 'Error in multiplyStrict: ');
+        return this.multiply(a, b);
+    };
+    NDArrayMath.prototype.divide = function (a, b) {
+        broadcast_util.assertAndGetBroadcastShape(a.shape, b.shape);
+        return this.backendEngine.executeKernel('Div', { inputs: { a: a, b: b } });
+    };
+    NDArrayMath.prototype.divideStrict = function (a, b) {
+        util.assertShapesMatch(a.shape, b.shape, 'Error in divideStrict: ');
+        return this.divide(a, b);
+    };
+    NDArrayMath.prototype.scalarDividedByArray = function (c, a) {
+        util.assert(c.size === 1, "Error in scalarDividedByArray: first argument must be rank 0, but " +
+            ("got NDArray of rank " + c.rank + "."));
+        return this.divide(c, a);
+    };
+    NDArrayMath.prototype.arrayDividedByScalar = function (a, c) {
+        util.assert(c.size === 1, "Error in arrayDividedByScalar: second argument must be rank 0, " +
+            ("but got NDArray of rank " + c.rank + "."));
+        return this.divide(a, c);
+    };
+    NDArrayMath.prototype.ceil = function (x) {
+        return this.backendEngine.executeKernel('Ceil', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.floor = function (x) {
+        return this.backendEngine.executeKernel('Floor', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.exp = function (x) {
+        return this.backendEngine.executeKernel('Exp', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.log = function (x) {
+        return this.backendEngine.executeKernel('Log', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.sqrt = function (x) {
+        return this.backendEngine.executeKernel('Sqrt', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.square = function (x) {
+        return this.backendEngine.executeKernel('Square', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.abs = function (x) {
+        return this.backendEngine.executeKernel('Abs', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.clip = function (x, min, max) {
+        util.assert((min <= max), "Error in clip: min (" + min + ") must be" +
+            ("less than or equal to max (" + max + ")."));
+        return this.backendEngine.executeKernel('Clip', { inputs: { x: x }, args: { min: min, max: max } });
+    };
+    NDArrayMath.prototype.relu = function (x) {
+        var _this = this;
+        return this.backendEngine.executeKernel('Relu', { inputs: { x: x } }, function (dy, y) {
+            return { x: function () { return _this.step(x); } };
+        });
+    };
+    NDArrayMath.prototype.elu = function (x) {
+        return this.backendEngine.executeKernel('Elu', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.eluDer = function (x) {
+        return this.backendEngine.executeKernel('EluDer', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.selu = function (x) {
+        return this.backendEngine.executeKernel('Selu', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.leakyRelu = function (x, alpha) {
+        if (alpha === void 0) { alpha = 0.2; }
+        return this.backendEngine.executeKernel('LeakyRelu', { inputs: { x: x }, args: { alpha: alpha } });
+    };
+    NDArrayMath.prototype.prelu = function (x, alpha) {
+        return this.backendEngine.executeKernel('PReLU', { inputs: { x: x, alpha: alpha } });
+    };
+    NDArrayMath.prototype.preluDer = function (x, alpha) {
+        return this.backendEngine.executeKernel('PReLUDer', { inputs: { x: x, alpha: alpha } });
+    };
+    NDArrayMath.prototype.sigmoid = function (x) {
+        return this.backendEngine.executeKernel('Sigmoid', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.sin = function (x) {
+        return this.backendEngine.executeKernel('Sin', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.cos = function (x) {
+        return this.backendEngine.executeKernel('Cos', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.tan = function (x) {
+        return this.backendEngine.executeKernel('Tan', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.asin = function (x) {
+        return this.backendEngine.executeKernel('Asin', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.acos = function (x) {
+        return this.backendEngine.executeKernel('Acos', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.atan = function (x) {
+        return this.backendEngine.executeKernel('Atan', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.sinh = function (x) {
+        return this.backendEngine.executeKernel('Sinh', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.cosh = function (x) {
+        return this.backendEngine.executeKernel('Cosh', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.tanh = function (x) {
+        return this.backendEngine.executeKernel('Tanh', { inputs: { x: x } });
+    };
+    NDArrayMath.prototype.step = function (x, alpha) {
+        if (alpha === void 0) { alpha = 0.0; }
+        return this.backendEngine.executeKernel('Step', { inputs: { x: x }, args: { alpha: alpha } });
+    };
+    NDArrayMath.prototype.scaledArrayAdd = function (c1, a, c2, b) {
+        var _this = this;
+        util.assert(c1.size === 1, "Error in scaledArrayAdd: first argument must rank 0, but got " +
+            (" rank " + c1.rank + "."));
+        util.assert(c2.size === 1, "Error in scaledArrayAdd: third argument must be rank 0, but got " +
+            ("NDArray of rank " + c2.rank + "."));
+        util.assertShapesMatch(a.shape, b.shape, 'Error in scaledArrayAdd: ');
+        return this.executeOp('scaledArrayAdd', function () {
+            return _this.scope(function () {
+                return _this.add(_this.multiply(c1, a), _this.multiply(c2, b));
+            });
+        });
+    };
+    NDArrayMath.prototype.scalarTimesArray = function (c, a) {
+        util.assert(c.size === 1, "Error in arrayDividedByScalar: first argument must be rank 0, but " +
+            ("got rank " + c.rank + "."));
+        return this.multiply(c, a);
+    };
+    NDArrayMath.prototype.elementWiseMulBroadcast = function (a, b) {
+        util.assert(a.rank === 2, "Error in elementWiseMulBroadcast: first argument must be " +
+            ("rank 2, but got rank " + a.rank + "."));
+        util.assert(b.rank === 2, "Error in elementWiseMulBroadcast: second argument must be " +
+            ("rank 2, but got rank " + b.rank + "."));
+        return this.multiply(a, b);
+    };
+    NDArrayMath.prototype.conv1d = function (input, filter, bias, stride, pad) {
+        var _this = this;
+        var input3D = input;
+        var reshapedTo3D = false;
+        if (input.rank === 2) {
+            reshapedTo3D = true;
+            input3D = input.as3D(1, input.shape[0], input.shape[1]);
+        }
+        util.assert(input3D.rank === 3, "Error in conv1d: input must be rank 3, but got rank " + input3D.rank + ".");
+        util.assert(filter.rank === 3, "Error in conv1d: filter must be rank 3, but got rank " +
+            (filter.rank + "."));
+        if (bias != null) {
+            util.assert(bias.rank === 1, "Error in conv1d: bias must be rank 1, but got rank " +
+                (bias.rank + "."));
+        }
+        util.assert(input3D.shape[2] === filter.shape[1], "Error in conv1d: depth of input (" + input3D.shape[2] + ") must match  " +
+            ("input depth for filter " + filter.shape[1] + "."));
+        var filter4D = filter.as4D(1, filter.shape[0], filter.shape[1], filter.shape[2]);
+        var input4D = input3D.as4D(input3D.shape[0], 1, input3D.shape[1], input3D.shape[2]);
+        var strides = [1, stride];
+        return this.executeOp('Conv1D', function () {
+            var res = _this.conv2d(input4D, filter4D, bias, strides, pad);
+            if (reshapedTo3D) {
+                return res.as2D(res.shape[2], res.shape[3]);
+            }
+            return res.as3D(res.shape[0], res.shape[2], res.shape[3]);
+        });
+    };
+    NDArrayMath.prototype.conv2d = function (input, filter, bias, strides, pad) {
+        var _this = this;
+        var input4D = input;
+        var reshapedTo4D = false;
+        if (input.rank === 3) {
+            reshapedTo4D = true;
+            input4D = input.as4D(1, input.shape[0], input.shape[1], input.shape[2]);
+        }
+        util.assert(input4D.rank === 4, "Error in conv2d: input must be rank 4, but got rank " + input4D.rank + ".");
+        util.assert(filter.rank === 4, "Error in conv2d: filter must be rank 4, but got rank " +
+            (filter.rank + "."));
+        if (bias != null) {
+            util.assert(bias.rank === 1, "Error in conv2d: bias must be rank 1, but got rank " +
+                (bias.rank + "."));
+        }
+        util.assert(input4D.shape[3] === filter.shape[2], "Error in conv2d: depth of input (" + input4D.shape[3] + ") must match  " +
+            ("input depth for filter " + filter.shape[2] + "."));
+        var convInfo = conv_util.computeConv2DInfo(input4D.shape, filter.shape, strides, pad);
+        return this.executeOp('Conv2D', function () {
+            var res = _this.backendEngine.executeKernel('Conv2D', { inputs: { x: input4D, filter: filter, bias: bias }, args: { convInfo: convInfo } });
+            if (reshapedTo4D) {
+                return res.as3D(res.shape[1], res.shape[2], res.shape[3]);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.conv2dDerInput = function (inShape, dy, filter, strides, pad) {
+        var _this = this;
+        util.assert(inShape.length === dy.rank, "Length of inShape " +
+            ("(" + inShape.length + ") and rank of dy (" + dy.rank + ") must match"));
+        var inShape4D = inShape;
+        var dy4D = dy;
+        var reshapedTo4D = false;
+        if (dy.rank === 3) {
+            reshapedTo4D = true;
+            dy4D = dy.as4D(1, dy.shape[0], dy.shape[1], dy.shape[2]);
+            inShape4D = [1, inShape[0], inShape[1], inShape[2]];
+        }
+        var inDepth = inShape4D[3];
+        var outDepth = dy4D.shape[3];
+        util.assert(inShape4D.length === 4, "Error in conv2dDerInput: inShape must be length 4, but got length " +
+            (inShape4D.length + "."));
+        util.assert(dy4D.rank === 4, "Error in conv2dDerInput: dy must be rank 4, but got " +
+            ("rank " + dy4D.rank));
+        util.assert(filter.rank === 4, "Error in conv2dDerInput: filter must be rank 4, but got " +
+            ("rank " + filter.rank));
+        util.assert(inDepth === filter.shape[2], "Error in conv2dDerInput: depth of input (" + inDepth + ") must " +
+            ("match input depth for filter " + filter.shape[2] + "."));
+        util.assert(outDepth === filter.shape[3], "Error in conv2dDerInput: depth of output (" + outDepth + ") must" +
+            ("match output depth for filter " + filter.shape[3] + "."));
+        var convInfo = conv_util.computeConv2DInfo(inShape4D, filter.shape, strides, pad);
+        return this.executeOp('conv2dDerInput', function () {
+            var res = _this.backendEngine.executeKernel('Conv2DDerInput', { inputs: { dy: dy4D, filter: filter }, args: { convInfo: convInfo } });
+            if (reshapedTo4D) {
+                return res.as3D(res.shape[1], res.shape[2], res.shape[3]);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.conv2dDerBias = function (dy) {
+        var dy4D = dy;
+        if (dy.rank === 3) {
+            dy4D = dy.as4D(1, dy.shape[0], dy.shape[1], dy.shape[2]);
+        }
+        return this.backendEngine.executeKernel('Conv2DDerBias', { inputs: { dy: dy4D } });
+    };
+    NDArrayMath.prototype.conv2dDerFilter = function (input, dy, filterShape, strides, pad) {
+        var input4D = input;
+        if (input.rank === 3) {
+            input4D = input.as4D(1, input.shape[0], input.shape[1], input.shape[2]);
+        }
+        var dy4D = dy;
+        if (dy4D.rank === 3) {
+            dy4D = dy.as4D(1, dy.shape[0], dy.shape[1], dy.shape[2]);
+        }
+        util.assert(input4D.rank === 4, "Error in conv2dDerFilter: input must be rank 4, but got shape " +
+            (input4D.shape + "."));
+        util.assert(dy4D.rank === 4, "Error in conv2dDerFilter: dy must be rank 4, but got shape " +
+            (dy4D.shape + "."));
+        util.assert(filterShape.length === 4, "Error in conv2dDerFilter: filterShape must be length 4, but got " +
+            (filterShape + "."));
+        util.assert(input4D.shape[3] === filterShape[2], "Error in conv2dDerFilter: depth of input " + input4D.shape[3] + ") must " +
+            ("match input depth in filter (" + filterShape[2] + "."));
+        util.assert(dy4D.shape[3] === filterShape[3], "Error in conv2dDerFilter: depth of dy (" + dy4D.shape[3] + ") must " +
+            ("match output depth for filter (" + filterShape[3] + ")."));
+        var convInfo = conv_util.computeConv2DInfo(input4D.shape, filterShape, strides, pad);
+        return this.backendEngine.executeKernel('Conv2DDerFilter', { inputs: { x: input4D, dy: dy4D }, args: { convInfo: convInfo } });
+    };
+    NDArrayMath.prototype.conv2dTranspose = function (x, filter, outputShape, strides, pad) {
+        return this.conv2dDerInput(outputShape, x, filter, strides, pad);
+    };
+    NDArrayMath.prototype.depthwiseConv2D = function (input, filter, strides, pad, rates) {
+        var _this = this;
+        if (rates === void 0) { rates = [1, 1]; }
+        var input4D = input;
+        var reshapedTo4D = false;
+        if (input.rank === 3) {
+            reshapedTo4D = true;
+            input4D = input.as4D(1, input.shape[0], input.shape[1], input.shape[2]);
+        }
+        util.assert(input4D.rank === 4, "Error in depthwiseConv2D: input must be rank 4, but got " +
+            ("rank " + input4D.rank + "."));
+        util.assert(filter.rank === 4, "Error in depthwiseConv2D: filter must be rank 4, but got rank " +
+            (filter.rank + "."));
+        util.assert(input4D.shape[3] === filter.shape[2], "Error in depthwiseConv2D: number of input channels " +
+            ("(" + input4D.shape[3] + ") must match the inChannels dimension in ") +
+            ("filter " + filter.shape[2] + "."));
+        rates = rates || [1, 1];
+        var _a = parseTupleParam(rates), rateHeight = _a[0], rateWidth = _a[1];
+        util.assert(rateHeight === 1 && rateWidth === 1, 'Error in depthwiseConv2D: rates greater than 1 are not yet ' +
+            ("supported. Got rates '" + rates + "'"));
+        var convInfo = conv_util.computeConv2DInfo(input4D.shape, filter.shape, strides, pad, true);
+        return this.executeOp('depthwiseConv2D', function () {
+            var res = _this.backendEngine.executeKernel('DepthwiseConv2D', { inputs: { x: input4D, filter: filter }, args: { convInfo: convInfo } });
+            if (reshapedTo4D) {
+                return res.as3D(res.shape[1], res.shape[2], res.shape[3]);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.maxPool = function (input, filterSize, strides, pad) {
+        var _this = this;
+        var input4D = input;
+        var reshapedTo4D = false;
+        if (input.rank === 3) {
+            reshapedTo4D = true;
+            input4D = input.as4D(1, input.shape[0], input.shape[1], input.shape[2]);
+        }
+        util.assert(input4D.rank === 4, "Error in maxPool: input must be rank 4 but got rank " + input4D.rank + ".");
+        var convInfo = conv_util.computePool2DInfo(input4D.shape, filterSize, strides, pad);
+        return this.executeOp('maxPool', function () {
+            var res = _this.backendEngine.executeKernel('MaxPool', { inputs: { x: input4D }, args: { convInfo: convInfo } });
+            if (reshapedTo4D) {
+                return res.as3D(res.shape[1], res.shape[2], res.shape[3]);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.maxPoolBackprop = function (dy, input, filterSize, strides, pad) {
+        var _this = this;
+        util.assert(input.rank === dy.rank, "Rank of input (" + input.rank + ") does not match rank of dy (" + dy.rank + ")");
+        var input4D = input;
+        var dy4D = dy;
+        var reshapedTo4D = false;
+        if (input.rank === 3) {
+            reshapedTo4D = true;
+            input4D = input.as4D(1, input.shape[0], input.shape[1], input.shape[2]);
+            dy4D = dy.as4D(1, dy.shape[0], dy.shape[1], dy.shape[2]);
+        }
+        util.assert(dy4D.rank === 4, "Error in maxPoolBackprop: dy must be rank 4 but got rank " +
+            (dy4D.rank + "."));
+        util.assert(input4D.rank === 4, "Error in maxPoolBackprop: input must be rank 4 but got rank " +
+            (input4D.rank + "."));
+        var convInfo = conv_util.computePool2DInfo(input4D.shape, filterSize, strides, pad);
+        return this.executeOp('maxPoolBackprop', function () {
+            var res = _this.backendEngine.executeKernel('MaxPoolBackprop', { inputs: { dy: dy4D, x: input4D }, args: { convInfo: convInfo } });
+            if (reshapedTo4D) {
+                return res.as3D(res.shape[1], res.shape[2], res.shape[3]);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.minPool = function (input, filterSize, strides, pad) {
+        var _this = this;
+        var input4D = input;
+        var reshapedTo4D = false;
+        if (input.rank === 3) {
+            reshapedTo4D = true;
+            input4D = input.as4D(1, input.shape[0], input.shape[1], input.shape[2]);
+        }
+        util.assert(input4D.rank === 4, "Error in minPool: x must be rank 4 but got rank " + input4D.rank + ".");
+        var convInfo = conv_util.computePool2DInfo(input4D.shape, filterSize, strides, pad);
+        return this.executeOp('minPool', function () {
+            var res = _this.backendEngine.executeKernel('MinPool', { inputs: { x: input4D }, args: { convInfo: convInfo } });
+            if (reshapedTo4D) {
+                return res.as3D(res.shape[1], res.shape[2], res.shape[3]);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.avgPool = function (input, filterSize, strides, pad) {
+        var _this = this;
+        var input4D = input;
+        var reshapedTo4D = false;
+        if (input.rank === 3) {
+            reshapedTo4D = true;
+            input4D = input.as4D(1, input.shape[0], input.shape[1], input.shape[2]);
+        }
+        util.assert(input4D.rank === 4, "Error in avgPool: x must be rank 4 but got rank " + input4D.rank + ".");
+        var convInfo = conv_util.computePool2DInfo(input4D.shape, filterSize, strides, pad);
+        return this.executeOp('avgPool', function () {
+            var res = _this.backendEngine.executeKernel('AvgPool', { inputs: { x: input4D }, args: { convInfo: convInfo } });
+            if (reshapedTo4D) {
+                return res.as3D(res.shape[1], res.shape[2], res.shape[3]);
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.resizeBilinear3D = function (x, newShape2D, alignCorners) {
+        if (alignCorners === void 0) { alignCorners = false; }
+        util.assert(x.rank === 3, "Error in resizeBilinear3D: x must be rank 3 but got rank " + x.rank + ".");
+        util.assert(newShape2D.length === 2, "Error in resizeBilinear3D: new shape must 2D, but got shape " +
+            (newShape2D + "."));
+        return this.backendEngine.executeKernel('ResizeBilinear3D', { inputs: { x: x }, args: { newShape2D: newShape2D, alignCorners: alignCorners } });
+    };
+    NDArrayMath.prototype.batchNormalization2D = function (x, mean, variance, varianceEpsilon, scale, offset) {
+        if (varianceEpsilon === void 0) { varianceEpsilon = .001; }
+        util.assert(x.rank === 2, "Error in batchNormalization3D: x must be rank 3 but got rank " +
+            (x.rank + "."));
+        util.assert(mean.rank === 2 || mean.rank === 1, "Error in batchNormalization2D: mean must be rank 2 or rank 1 but " +
+            ("got rank " + mean.rank + "."));
+        util.assert(variance.rank === 2 || variance.rank === 1, "Error in batchNormalization2D: variance must be rank 2 or rank 1 " +
+            ("but got rank " + variance.rank + "."));
+        if (scale != null) {
+            util.assert(scale.rank === 2 || scale.rank === 1, "Error in batchNormalization2D: scale must be rank 2 or rank 1 " +
+                ("but got rank " + scale.rank + "."));
+        }
+        if (offset != null) {
+            util.assert(offset.rank === 2 || offset.rank === 1, "Error in batchNormalization2D: offset must be rank 2 or rank 1 " +
+                ("but got rank " + offset.rank + "."));
+        }
+        return this.backendEngine.executeKernel('BatchNorm2D', { inputs: { x: x, mean: mean, variance: variance, scale: scale, offset: offset }, args: { varianceEpsilon: varianceEpsilon } });
+    };
+    NDArrayMath.prototype.batchNormalization3D = function (x, mean, variance, varianceEpsilon, scale, offset) {
+        if (varianceEpsilon === void 0) { varianceEpsilon = .001; }
+        util.assert(x.rank === 3, "Error in batchNormalization3D: x must be rank 3 but got rank " +
+            (x.rank + "."));
+        util.assert(mean.rank === 3 || mean.rank === 1, "Error in batchNormalization3D: mean must be rank 3 or rank 1 but " +
+            ("got rank " + mean.rank + "."));
+        util.assert(variance.rank === 3 || variance.rank === 1, "Error in batchNormalization3D: variance must be rank 3 or rank 1 " +
+            ("but got rank " + variance.rank + "."));
+        if (scale != null) {
+            util.assert(scale.rank === 3 || scale.rank === 1, "Error in batchNormalization3D: scale must be rank 3 or rank 1 " +
+                ("but got rank " + scale.rank + "."));
+        }
+        if (offset != null) {
+            util.assert(offset.rank === 3 || offset.rank === 1, "Error in batchNormalization3D: offset must be rank 3 or rank 1 " +
+                ("but got rank " + offset.rank + "."));
+        }
+        return this.backendEngine.executeKernel('BatchNorm3D', { inputs: { x: x, mean: mean, variance: variance, scale: scale, offset: offset }, args: { varianceEpsilon: varianceEpsilon } });
+    };
+    NDArrayMath.prototype.multiRNNCell = function (lstmCells, data, c, h) {
+        var res = this.scope(function () {
+            var input = data;
+            var newStates = [];
+            for (var i = 0; i < lstmCells.length; i++) {
+                var output = lstmCells[i](input, c[i], h[i]);
+                newStates.push(output[0]);
+                newStates.push(output[1]);
+                input = output[1];
+            }
+            return newStates;
+        });
+        var newC = [];
+        var newH = [];
+        for (var i = 0; i < res.length; i += 2) {
+            newC.push(res[i]);
+            newH.push(res[i + 1]);
+        }
+        return [newC, newH];
+    };
+    NDArrayMath.prototype.basicLSTMCell = function (forgetBias, lstmKernel, lstmBias, data, c, h) {
+        var _this = this;
+        var res = this.scope(function () {
+            var combined = _this.concat2D(data, h, 1);
+            var weighted = _this.matMul(combined, lstmKernel);
+            var res = _this.add(weighted, lstmBias);
+            var batchSize = res.shape[0];
+            var sliceCols = res.shape[1] / 4;
+            var sliceSize = [batchSize, sliceCols];
+            var i = _this.slice2D(res, [0, 0], sliceSize);
+            var j = _this.slice2D(res, [0, sliceCols], sliceSize);
+            var f = _this.slice2D(res, [0, sliceCols * 2], sliceSize);
+            var o = _this.slice2D(res, [0, sliceCols * 3], sliceSize);
+            var newC = _this.addStrict(_this.multiplyStrict(c, _this.sigmoid(_this.scalarPlusArray(forgetBias, f))), _this.multiplyStrict(_this.sigmoid(i), _this.tanh(j)));
+            var newH = _this.multiplyStrict(_this.tanh(newC), _this.sigmoid(o));
+            return [newC, newH];
+        });
+        return [res[0], res[1]];
+    };
+    NDArrayMath.prototype.multinomial = function (probabilities, numSamples, seed) {
+        var _this = this;
+        var numOutcomes = probabilities.size;
+        if (numOutcomes < 2) {
+            throw new Error("Error in multinomial: you need at least 2 outcomes, but got " +
+                (numOutcomes + "."));
+        }
+        if (probabilities.rank > 2) {
+            throw new Error("Rank of probabilities must be 1 or 2, but is " + probabilities.rank);
+        }
+        seed = seed || Math.random();
+        var origRank = probabilities.rank;
+        if (probabilities.rank === 1) {
+            probabilities = probabilities.as2D(1, -1);
+        }
+        return this.executeOp('multinomial', function () {
+            var res = _this.backendEngine.executeKernel('Multinomial', {
+                inputs: { probs: probabilities },
+                args: { numSamples: numSamples, seed: seed }
+            });
+            if (origRank === 1) {
+                return res.as1D();
+            }
+            return res;
+        });
+    };
+    NDArrayMath.prototype.oneHot = function (indices, depth, onValue, offValue) {
+        if (onValue === void 0) { onValue = 1; }
+        if (offValue === void 0) { offValue = 0; }
+        if (depth < 2) {
+            throw new Error("Error in oneHot: depth must be >=2, but it is " + depth);
+        }
+        return this.backendEngine.executeKernel('OneHot', { inputs: { indices: indices }, args: { depth: depth, onValue: onValue, offValue: offValue } });
+    };
+    NDArrayMath.prototype.moments = function (x, axis, keepDims) {
+        var _this = this;
+        if (axis === void 0) { axis = null; }
+        if (keepDims === void 0) { keepDims = false; }
+        var axes = axis_util.parseAxisParam(axis, x.shape);
+        var result = this.scope(function () {
+            var mean = _this.mean(x, axes, keepDims);
+            var keepDimsShape = mean.shape;
+            if (!keepDims) {
+                keepDimsShape = axis_util.expandShapeToKeepDim(mean.shape, axes);
+            }
+            var devSquared = _this.square(_this.subtract(x, mean.reshape(keepDimsShape)));
+            var variance = _this.mean(devSquared, axes, keepDims);
+            return { mean: mean, variance: variance };
+        });
+        return result;
+    };
+    NDArrayMath.prototype.norm = function (x, ord, axis, keepDims) {
+        var _this = this;
+        if (ord === void 0) { ord = 'euclidean'; }
+        if (axis === void 0) { axis = null; }
+        if (keepDims === void 0) { keepDims = false; }
+        return this.scope(function () {
+            var norm = _this.normInternal(x, ord, axis);
+            var keepDimsShape = norm.shape;
+            if (keepDims) {
+                var axes = axis_util.parseAxisParam(axis, x.shape);
+                keepDimsShape = axis_util.expandShapeToKeepDim(norm.shape, axes);
+            }
+            return norm.reshape(keepDimsShape);
+        });
+    };
+    NDArrayMath.prototype.normInternal = function (x, p, axis) {
+        if (axis === void 0) { axis = null; }
+        if (x.rank === 0) {
+            return this.abs(x);
+        }
+        if (x.rank !== 1 && axis === null) {
+            return this.normInternal(x.reshape([-1]), p, axis);
+        }
+        if (x.rank === 1 || typeof axis === 'number' ||
+            axis instanceof Array && axis.length === 1) {
+            if (p === 1) {
+                return this.sum(this.abs(x), axis);
+            }
+            if (p === Infinity) {
+                return this.max(this.abs(x), axis);
+            }
+            if (p === -Infinity) {
+                return this.min(this.abs(x), axis);
+            }
+            if (p === 'euclidean' || p === 2) {
+                return this.sqrt(this.sum(this.pow(this.abs(x), ndarray_1.Scalar.new(2, 'int32')), axis));
+            }
+            throw new Error("Error in norm: invalid ord value: " + p);
+        }
+        if (axis instanceof Array && axis.length === 2) {
+            if (p === 1) {
+                return this.max(this.sum(this.abs(x), axis[0]), axis[1] - 1);
+            }
+            if (p === Infinity) {
+                return this.max(this.sum(this.abs(x), axis[1]), axis[0]);
+            }
+            if (p === -Infinity) {
+                return this.min(this.sum(this.abs(x), axis[1]), axis[0]);
+            }
+            if (p === 'fro' || p === 'euclidean') {
+                return this.sqrt(this.sum(this.pow(x, ndarray_1.Scalar.new(2, 'int32')), axis));
+            }
+            throw new Error("Error in norm: invalid ord value: " + p);
+        }
+        throw new Error("Error in norm: invalid axis: " + axis);
+    };
+    NDArrayMath.prototype.gradientWrt = function (y, x) {
+        var xIsArray = x instanceof ndarray_1.NDArray;
+        var xs = [];
+        var xKeys;
+        if (xIsArray) {
+            xs.push(x);
+        }
+        else {
+            var xMap = x;
+            xKeys = Object.keys(xMap);
+            for (var i = 0; i < xKeys.length; i++) {
+                xs.push(xMap[xKeys[i]]);
+            }
+        }
+        var gradients = this.backendEngine.gradientWrt(y, xs);
+        if (xIsArray) {
+            return gradients[0];
+        }
+        else {
+            var result = {};
+            for (var i = 0; i < xKeys.length; i++) {
+                result[xKeys[i]] = gradients[i];
+            }
+            return result;
+        }
+    };
+    NDArrayMath.prototype.disposeData = function (id) {
+        this.backend.disposeData(id);
+        this.numArrays--;
+    };
+    return NDArrayMath;
+}());
+exports.NDArrayMath = NDArrayMath;
+function parseTupleParam(param) {
+    return typeof param === 'number' ? [param, param] : param;
+}
+
+},{"../environment":15,"../util":101,"./axis_util":54,"./backends/backend_engine":56,"./backends/types/matmul":61,"./broadcast_util":90,"./concat_util":91,"./conv_util":92,"./ndarray":95,"./slice_util":98}],95:[function(require,module,exports){
+"use strict";
+var __extends = (this && this.__extends) || (function () {
+    var extendStatics = Object.setPrototypeOf ||
+        ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
+        function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
+    return function (d, b) {
+        extendStatics(d, b);
+        function __() { this.constructor = d; }
+        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
+    };
+})();
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __generator = (this && this.__generator) || function (thisArg, body) {
+    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
+    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
+    function verb(n) { return function (v) { return step([n, v]); }; }
+    function step(op) {
+        if (f) throw new TypeError("Generator is already executing.");
+        while (_) try {
+            if (f = 1, y && (t = y[op[0] & 2 ? "return" : op[0] ? "throw" : "next"]) && !(t = t.call(y, op[1])).done) return t;
+            if (y = 0, t) op = [0, t.value];
+            switch (op[0]) {
+                case 0: case 1: t = op; break;
+                case 4: _.label++; return { value: op[1], done: false };
+                case 5: _.label++; y = op[1]; op = [0]; continue;
+                case 7: op = _.ops.pop(); _.trys.pop(); continue;
+                default:
+                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
+                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
+                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
+                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
+                    if (t[2]) _.ops.pop();
+                    _.trys.pop(); continue;
+            }
+            op = body.call(thisArg, _);
+        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
+        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
+    }
+};
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("../environment");
+var util = require("../util");
+var rand_1 = require("./rand");
+var DType;
+(function (DType) {
+    DType["float32"] = "float32";
+    DType["int32"] = "int32";
+    DType["bool"] = "bool";
+})(DType = exports.DType || (exports.DType = {}));
+var NDArray = (function () {
+    function NDArray(shape, dtype, values, id, math) {
+        this.isDisposed = false;
+        this.math = math || environment_1.ENV.math;
+        this.size = util.sizeFromShape(shape);
+        if (values != null) {
+            util.assert(this.size === values.length, "Constructing ndarray of shape (" + this.size + ") should match the " +
+                ("length of values (" + values.length + ")"));
+        }
+        this.shape = shape;
+        this.dtype = dtype || 'float32';
+        var dim = this.shape.length;
+        if (dim < 2) {
+            this.strides = [];
+        }
+        else {
+            this.strides = new Array(dim - 1);
+            this.strides[dim - 2] = this.shape[dim - 1];
+            for (var i = dim - 3; i >= 0; --i) {
+                this.strides[i] = this.strides[i + 1] * this.shape[i + 1];
+            }
+        }
+        this.id = id;
+        if (this.id == null) {
+            this.id = NDArray.nextId++;
+            this.math.register(this);
+            this.math.write(this.id, values, this.dtype, this.shape);
+        }
+    }
+    NDArray.ones = function (shape, dtype) {
+        var values = makeOnesTypedArray(util.sizeFromShape(shape), dtype);
+        return NDArray.make(shape, { values: values }, dtype);
+    };
+    NDArray.zeros = function (shape, dtype) {
+        var values = makeZerosTypedArray(util.sizeFromShape(shape), dtype);
+        return NDArray.make(shape, { values: values }, dtype);
+    };
+    NDArray.onesLike = function (another) {
+        return NDArray.ones(another.shape, another.dtype);
+    };
+    NDArray.zerosLike = function (another) {
+        return NDArray.zeros(another.shape, another.dtype);
+    };
+    NDArray.like = function (another) {
+        var newValues = copyTypedArray(another.getValues(), another.dtype);
+        return NDArray.make(another.shape, { values: newValues }, another.dtype);
+    };
+    NDArray.make = function (shape, data, dtype, math) {
+        switch (shape.length) {
+            case 0:
+                return new Scalar(shape, dtype, data.values, data.id, math);
+            case 1:
+                return new Array1D(shape, dtype, data.values, data.id, math);
+            case 2:
+                return new Array2D(shape, dtype, data.values, data.id, math);
+            case 3:
+                return new Array3D(shape, dtype, data.values, data.id, math);
+            case 4:
+                return new Array4D(shape, dtype, data.values, data.id, math);
+            default:
+                return new NDArray(shape, dtype, data.values, data.id, math);
+        }
+    };
+    NDArray.fromPixels = function (pixels, numChannels, math) {
+        if (numChannels === void 0) { numChannels = 3; }
+        if (numChannels > 4) {
+            throw new Error('Cannot construct NDArray with more than 4 channels from pixels.');
+        }
+        var ndarrayData = {};
+        var shape = [pixels.height, pixels.width, numChannels];
+        var res = NDArray.make(shape, ndarrayData, 'int32');
+        math = math || environment_1.ENV.math;
+        math.writePixels(res.id, pixels, numChannels);
+        return res;
+    };
+    NDArray.prototype.reshape = function (newShape) {
+        this.throwIfDisposed();
+        newShape = util.inferFromImplicitShape(newShape, this.size);
+        if (util.arraysEqual(this.shape, newShape)) {
+            return this;
+        }
+        var data = { id: this.id };
+        util.assert(this.size === util.sizeFromShape(newShape), 'new shape and old shape must have the same number of elements.');
+        return NDArray.make(newShape, data, this.dtype);
+    };
+    NDArray.prototype.flatten = function () {
+        this.throwIfDisposed();
+        if (this instanceof Array1D) {
+            return this;
+        }
+        return this.as1D();
+    };
+    NDArray.prototype.asScalar = function () {
+        this.throwIfDisposed();
+        util.assert(this.size === 1, 'The array must have only 1 element.');
+        return this.reshape([]);
+    };
+    NDArray.prototype.as1D = function () {
+        this.throwIfDisposed();
+        return this.reshape([this.size]);
+    };
+    NDArray.prototype.as2D = function (rows, columns) {
+        this.throwIfDisposed();
+        return this.reshape([rows, columns]);
+    };
+    NDArray.prototype.as3D = function (rows, columns, depth) {
+        this.throwIfDisposed();
+        return this.reshape([rows, columns, depth]);
+    };
+    NDArray.prototype.as4D = function (rows, columns, depth, depth2) {
+        this.throwIfDisposed();
+        return this.reshape([rows, columns, depth, depth2]);
+    };
+    NDArray.prototype.asType = function (dtype) {
+        this.throwIfDisposed();
+        if (this.dtype === dtype) {
+            return this;
+        }
+        var vals = this.dataSync();
+        var newVals = toTypedArray(vals, dtype);
+        return NDArray.make(this.shape, { values: newVals }, dtype);
+    };
+    Object.defineProperty(NDArray.prototype, "rank", {
+        get: function () {
+            return this.shape.length;
+        },
+        enumerable: true,
+        configurable: true
+    });
+    NDArray.prototype.get = function () {
+        var locs = [];
+        for (var _i = 0; _i < arguments.length; _i++) {
+            locs[_i] = arguments[_i];
+        }
+        var index = locs[locs.length - 1];
+        for (var i = 0; i < locs.length - 1; ++i) {
+            index += this.strides[i] * locs[i];
+        }
+        return this.getValues()[index];
+    };
+    NDArray.prototype.add = function (value) {
+        var locs = [];
+        for (var _i = 1; _i < arguments.length; _i++) {
+            locs[_i - 1] = arguments[_i];
+        }
+        this.set.apply(this, [this.get.apply(this, locs) + value].concat(locs));
+    };
+    NDArray.prototype.set = function (value) {
+        var locs = [];
+        for (var _i = 1; _i < arguments.length; _i++) {
+            locs[_i - 1] = arguments[_i];
+        }
+        this.throwIfDisposed();
+        util.assert(locs.length === this.rank, "The number of provided coordinates (" + locs.length + ") must " +
+            ("match the rank (" + this.rank + ")"));
+        var index = locs.length > 0 ? locs[locs.length - 1] : 0;
+        for (var i = 0; i < locs.length - 1; ++i) {
+            index += this.strides[i] * locs[i];
+        }
+        var vals = this.getValues();
+        vals[index] = value;
+        this.math.disposeData(this.id);
+        this.math.write(this.id, vals, this.dtype, this.shape);
+    };
+    NDArray.prototype.val = function () {
+        var locs = [];
+        for (var _i = 0; _i < arguments.length; _i++) {
+            locs[_i] = arguments[_i];
+        }
+        return __awaiter(this, void 0, void 0, function () {
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0:
+                        this.throwIfDisposed();
+                        return [4, this.data()];
+                    case 1:
+                        _a.sent();
+                        return [2, this.get.apply(this, locs)];
+                }
+            });
+        });
+    };
+    NDArray.prototype.locToIndex = function (locs) {
+        this.throwIfDisposed();
+        var index = locs[locs.length - 1];
+        for (var i = 0; i < locs.length - 1; ++i) {
+            index += this.strides[i] * locs[i];
+        }
+        return index;
+    };
+    NDArray.prototype.indexToLoc = function (index) {
+        this.throwIfDisposed();
+        var locs = new Array(this.shape.length);
+        for (var i = 0; i < locs.length - 1; ++i) {
+            locs[i] = Math.floor(index / this.strides[i]);
+            index -= locs[i] * this.strides[i];
+        }
+        locs[locs.length - 1] = index;
+        return locs;
+    };
+    NDArray.prototype.fill = function (value) {
+        this.throwIfDisposed();
+        var vals = this.getValues();
+        vals.fill(value);
+        this.math.disposeData(this.id);
+        this.math.write(this.id, vals, this.dtype, this.shape);
+    };
+    NDArray.prototype.getValues = function () {
+        return this.dataSync();
+    };
+    NDArray.prototype.getValuesAsync = function () {
+        return this.data();
+    };
+    NDArray.prototype.data = function () {
+        return __awaiter(this, void 0, void 0, function () {
+            return __generator(this, function (_a) {
+                this.throwIfDisposed();
+                return [2, this.math.read(this.id)];
+            });
+        });
+    };
+    NDArray.prototype.dataSync = function () {
+        this.throwIfDisposed();
+        return this.math.readSync(this.id);
+    };
+    NDArray.prototype.dispose = function () {
+        this.isDisposed = true;
+        this.math.disposeData(this.id);
+    };
+    NDArray.prototype.equals = function (t) {
+        this.throwIfDisposed();
+        return this.dtype === t.dtype && util.arraysEqual(this.shape, t.shape) &&
+            util.arraysEqual(this.getValues(), t.getValues());
+    };
+    NDArray.rand = function (shape, randFunction, dtype) {
+        var size = util.sizeFromShape(shape);
+        var values = null;
+        if (dtype == null || dtype === 'float32') {
+            values = new Float32Array(size);
+        }
+        else if (dtype === 'int32') {
+            values = new Int32Array(size);
+        }
+        else if (dtype === 'bool') {
+            values = new Uint8Array(size);
+        }
+        else {
+            throw new Error("Unknown data type " + dtype);
+        }
+        for (var i = 0; i < size; i++) {
+            values[i] = randFunction();
+        }
+        return NDArray.make(shape, { values: values }, dtype);
+    };
+    NDArray.randNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, false, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    NDArray.randTruncatedNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, true, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    NDArray.randUniform = function (shape, a, b, dtype) {
+        return NDArray.rand(shape, function () { return util.randUniform(a, b); }, dtype);
+    };
+    NDArray.prototype.throwIfDisposed = function () {
+        if (this.isDisposed) {
+            throw new Error("NDArray is disposed.");
+        }
+    };
+    NDArray.nextId = 0;
+    return NDArray;
+}());
+exports.NDArray = NDArray;
+var Scalar = (function (_super) {
+    __extends(Scalar, _super);
+    function Scalar() {
+        return _super !== null && _super.apply(this, arguments) || this;
+    }
+    Scalar.new = function (value, dtype) {
+        var values = [value];
+        return new Scalar([], dtype, toTypedArray(values, dtype));
+    };
+    Scalar.prototype.get = function () {
+        return this.getValues()[0];
+    };
+    Scalar.prototype.val = function () {
+        return __awaiter(this, void 0, void 0, function () {
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0: return [4, this.data()];
+                    case 1:
+                        _a.sent();
+                        return [2, this.get()];
+                }
+            });
+        });
+    };
+    Scalar.prototype.add = function (value) {
+        this.getValues()[0] += value;
+    };
+    Scalar.prototype.asType = function (dtype) {
+        return _super.prototype.asType.call(this, dtype);
+    };
+    Scalar.prototype.locToIndex = function (loc) {
+        return 0;
+    };
+    Scalar.prototype.indexToLoc = function (index) {
+        return [];
+    };
+    return Scalar;
+}(NDArray));
+exports.Scalar = Scalar;
+var Array1D = (function (_super) {
+    __extends(Array1D, _super);
+    function Array1D() {
+        return _super !== null && _super.apply(this, arguments) || this;
+    }
+    Array1D.new = function (values, dtype) {
+        if (!instanceofTypedArray(values)) {
+            var inferredShape = util.inferShape(values);
+            util.assert(inferredShape.length === 1, "Error constructing Array1D. Shape of values " + inferredShape + " is " +
+                "not 1 dimensional.");
+        }
+        return new Array1D([values.length], dtype, toTypedArray(values, dtype));
+    };
+    Array1D.prototype.get = function (i) {
+        return this.getValues()[i];
+    };
+    Array1D.prototype.val = function (i) {
+        return __awaiter(this, void 0, void 0, function () {
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0: return [4, this.data()];
+                    case 1:
+                        _a.sent();
+                        return [2, this.get(i)];
+                }
+            });
+        });
+    };
+    Array1D.prototype.add = function (value, i) {
+        this.getValues()[i] += value;
+    };
+    Array1D.prototype.locToIndex = function (loc) {
+        return loc[0];
+    };
+    Array1D.prototype.indexToLoc = function (index) {
+        return [index];
+    };
+    Array1D.prototype.asType = function (dtype) {
+        return _super.prototype.asType.call(this, dtype);
+    };
+    Array1D.ones = function (shape, dtype) {
+        return NDArray.ones(shape, dtype);
+    };
+    Array1D.zeros = function (shape, dtype) {
+        return NDArray.zeros(shape, dtype);
+    };
+    Array1D.randNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, false, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    Array1D.randTruncatedNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, true, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    Array1D.randUniform = function (shape, a, b, dtype) {
+        return NDArray.rand(shape, function () { return util.randUniform(a, b); }, dtype);
+    };
+    return Array1D;
+}(NDArray));
+exports.Array1D = Array1D;
+var Array2D = (function (_super) {
+    __extends(Array2D, _super);
+    function Array2D(shape, dtype, values, id, math) {
+        var _this = this;
+        util.assert(shape.length === 2, 'Shape should be of length 2');
+        _this = _super.call(this, shape, dtype, values, id, math) || this;
+        _this.stride0 = _this.strides[0];
+        return _this;
+    }
+    Array2D.new = function (shape, values, dtype) {
+        if (!instanceofTypedArray(values)) {
+            var inferredShape = util.inferShape(values);
+            if (inferredShape.length > 1) {
+                util.assertShapesMatch(shape, inferredShape, "Error when constructing Array2D. Shape of values " +
+                    (inferredShape + " does not match the provided shape ") +
+                    (shape + ". "));
+            }
+        }
+        return new Array2D(shape, dtype, toTypedArray(values, dtype));
+    };
+    Array2D.prototype.get = function (i, j) {
+        return this.getValues()[this.stride0 * i + j];
+    };
+    Array2D.prototype.add = function (value, i, j) {
+        this.getValues()[this.stride0 * i + j] += value;
+    };
+    Array2D.prototype.val = function (i, j) {
+        return __awaiter(this, void 0, void 0, function () {
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0: return [4, this.data()];
+                    case 1:
+                        _a.sent();
+                        return [2, this.get(i, j)];
+                }
+            });
+        });
+    };
+    Array2D.prototype.locToIndex = function (locs) {
+        return this.stride0 * locs[0] + locs[1];
+    };
+    Array2D.prototype.indexToLoc = function (index) {
+        return [Math.floor(index / this.stride0), index % this.stride0];
+    };
+    Array2D.prototype.asType = function (dtype) {
+        return _super.prototype.asType.call(this, dtype);
+    };
+    Array2D.ones = function (shape, dtype) {
+        return NDArray.ones(shape, dtype);
+    };
+    Array2D.zeros = function (shape, dtype) {
+        return NDArray.zeros(shape, dtype);
+    };
+    Array2D.randNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, false, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    Array2D.randTruncatedNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, true, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    Array2D.randUniform = function (shape, a, b, dtype) {
+        return NDArray.rand(shape, function () { return util.randUniform(a, b); }, dtype);
+    };
+    return Array2D;
+}(NDArray));
+exports.Array2D = Array2D;
+var Array3D = (function (_super) {
+    __extends(Array3D, _super);
+    function Array3D(shape, dtype, values, id, math) {
+        var _this = this;
+        util.assert(shape.length === 3, 'Shape should be of length 3');
+        _this = _super.call(this, shape, dtype, values, id, math) || this;
+        _this.stride0 = _this.strides[0];
+        _this.stride1 = _this.strides[1];
+        return _this;
+    }
+    Array3D.new = function (shape, values, dtype) {
+        if (!instanceofTypedArray(values)) {
+            var inferredShape = util.inferShape(values);
+            if (inferredShape.length > 1) {
+                util.assertShapesMatch(shape, inferredShape, "Error when constructing Array3D. Shape of values " +
+                    (inferredShape + " does not match the provided shape ") +
+                    (shape + ". "));
+            }
+        }
+        return new Array3D(shape, dtype, toTypedArray(values, dtype));
+    };
+    Array3D.prototype.get = function (i, j, k) {
+        return this.getValues()[this.stride0 * i + this.stride1 * j + k];
+    };
+    Array3D.prototype.val = function (i, j, k) {
+        return __awaiter(this, void 0, void 0, function () {
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0: return [4, this.data()];
+                    case 1:
+                        _a.sent();
+                        return [2, this.get(i, j, k)];
+                }
+            });
+        });
+    };
+    Array3D.prototype.add = function (value, i, j, k) {
+        this.getValues()[this.stride0 * i + this.stride1 * j + k] += value;
+    };
+    Array3D.prototype.locToIndex = function (locs) {
+        return this.stride0 * locs[0] + this.stride1 * locs[1] + locs[2];
+    };
+    Array3D.prototype.indexToLoc = function (index) {
+        var i = Math.floor(index / this.stride0);
+        index -= i * this.stride0;
+        return [i, Math.floor(index / this.stride1), index % this.stride1];
+    };
+    Array3D.prototype.asType = function (dtype) {
+        return _super.prototype.asType.call(this, dtype);
+    };
+    Array3D.ones = function (shape, dtype) {
+        return NDArray.ones(shape, dtype);
+    };
+    Array3D.zeros = function (shape, dtype) {
+        return NDArray.zeros(shape, dtype);
+    };
+    Array3D.randNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, false, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    Array3D.randTruncatedNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, true, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    Array3D.randUniform = function (shape, a, b, dtype) {
+        return NDArray.rand(shape, function () { return util.randUniform(a, b); }, dtype);
+    };
+    return Array3D;
+}(NDArray));
+exports.Array3D = Array3D;
+var Array4D = (function (_super) {
+    __extends(Array4D, _super);
+    function Array4D(shape, dtype, values, id, math) {
+        var _this = this;
+        util.assert(shape.length === 4, 'Shape should be of length 4');
+        _this = _super.call(this, shape, dtype, values, id, math) || this;
+        _this.stride0 = _this.strides[0];
+        _this.stride1 = _this.strides[1];
+        _this.stride2 = _this.strides[2];
+        return _this;
+    }
+    Array4D.new = function (shape, values, dtype) {
+        if (!instanceofTypedArray(values)) {
+            var inferredShape = util.inferShape(values);
+            if (inferredShape.length > 1) {
+                util.assertShapesMatch(shape, inferredShape, "Error when constructing Array4D. Shape of values " +
+                    (inferredShape + " does not match the provided shape ") +
+                    (shape + ". "));
+            }
+        }
+        return new Array4D(shape, dtype, toTypedArray(values, dtype));
+    };
+    Array4D.prototype.get = function (i, j, k, l) {
+        return this.getValues()[this.stride0 * i + this.stride1 * j + this.stride2 * k + l];
+    };
+    Array4D.prototype.val = function (i, j, k, l) {
+        return __awaiter(this, void 0, void 0, function () {
+            return __generator(this, function (_a) {
+                switch (_a.label) {
+                    case 0: return [4, this.data()];
+                    case 1:
+                        _a.sent();
+                        return [2, this.get(i, j, k, l)];
+                }
+            });
+        });
+    };
+    Array4D.prototype.add = function (value, i, j, k, l) {
+        this.getValues()[this.stride0 * i + this.stride1 * j + this.stride2 * k + l] += value;
+    };
+    Array4D.prototype.locToIndex = function (locs) {
+        return this.stride0 * locs[0] + this.stride1 * locs[1] +
+            this.stride2 * locs[2] + locs[3];
+    };
+    Array4D.prototype.indexToLoc = function (index) {
+        var i = Math.floor(index / this.stride0);
+        index -= i * this.stride0;
+        var j = Math.floor(index / this.stride1);
+        index -= j * this.stride1;
+        return [i, j, Math.floor(index / this.stride2), index % this.stride2];
+    };
+    Array4D.prototype.asType = function (dtype) {
+        return _super.prototype.asType.call(this, dtype);
+    };
+    Array4D.ones = function (shape, dtype) {
+        return NDArray.ones(shape, dtype);
+    };
+    Array4D.zeros = function (shape, dtype) {
+        return NDArray.zeros(shape, dtype);
+    };
+    Array4D.randNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, false, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    Array4D.randTruncatedNormal = function (shape, mean, stdDev, dtype, seed) {
+        if (mean === void 0) { mean = 0; }
+        if (stdDev === void 0) { stdDev = 1; }
+        if (dtype != null && dtype === 'bool') {
+            throw new Error("Unsupported data type " + dtype);
+        }
+        var randGauss = new rand_1.MPRandGauss(mean, stdDev, dtype, true, seed);
+        return NDArray.rand(shape, function () { return randGauss.nextValue(); }, dtype);
+    };
+    Array4D.randUniform = function (shape, a, b, dtype) {
+        return NDArray.rand(shape, function () { return util.randUniform(a, b); }, dtype);
+    };
+    return Array4D;
+}(NDArray));
+exports.Array4D = Array4D;
+function copyTypedArray(array, dtype) {
+    if (dtype == null || dtype === 'float32') {
+        return new Float32Array(array);
+    }
+    else if (dtype === 'int32') {
+        var vals = new Int32Array(array.length);
+        for (var i = 0; i < vals.length; ++i) {
+            var val = array[i];
+            if (util.isValNaN(val, 'int32')) {
+                vals[i] = util.getNaN('int32');
+            }
+            else {
+                vals[i] = val;
+            }
+        }
+        return vals;
+    }
+    else if (dtype === 'bool') {
+        var bool = new Uint8Array(array.length);
+        for (var i = 0; i < bool.length; ++i) {
+            var val = array[i];
+            if (util.isValNaN(val, 'bool')) {
+                bool[i] = util.getNaN('bool');
+            }
+            else if (Math.round(val) !== 0) {
+                bool[i] = 1;
+            }
+        }
+        return bool;
+    }
+    else {
+        throw new Error("Unknown data type " + dtype);
+    }
+}
+function instanceofTypedArray(a) {
+    return a instanceof Float32Array || a instanceof Int32Array ||
+        a instanceof Uint8Array;
+}
+function noConversionNeeded(a, dtype) {
+    return (a instanceof Float32Array && dtype === 'float32') ||
+        (a instanceof Int32Array && dtype === 'int32') ||
+        (a instanceof Uint8Array && dtype === 'bool');
+}
+function toTypedArray(a, dtype) {
+    if (noConversionNeeded(a, dtype)) {
+        return a;
+    }
+    if (Array.isArray(a)) {
+        a = util.flatten(a);
+    }
+    return copyTypedArray(a, dtype);
+}
+function makeZerosTypedArray(size, dtype) {
+    if (dtype == null || dtype === 'float32') {
+        return new Float32Array(size);
+    }
+    else if (dtype === 'int32') {
+        return new Int32Array(size);
+    }
+    else if (dtype === 'bool') {
+        return new Uint8Array(size);
+    }
+    else {
+        throw new Error("Unknown data type " + dtype);
+    }
+}
+function makeOnesTypedArray(size, dtype) {
+    var array = makeZerosTypedArray(size, dtype);
+    for (var i = 0; i < array.length; i++) {
+        array[i] = 1;
+    }
+    return array;
+}
+
+},{"../environment":15,"../util":101,"./rand":96}],96:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var seedrandom = require("seedrandom");
+var MPRandGauss = (function () {
+    function MPRandGauss(mean, stdDeviation, dtype, truncated, seed) {
+        this.mean = mean;
+        this.stdDev = stdDeviation;
+        this.dtype = dtype;
+        this.nextVal = NaN;
+        this.truncated = truncated;
+        if (this.truncated) {
+            this.upper = this.mean + this.stdDev * 2;
+            this.lower = this.mean - this.stdDev * 2;
+        }
+        var seedValue = seed ? seed : Math.random();
+        this.random = seedrandom.alea(seedValue.toString());
+    }
+    MPRandGauss.prototype.nextValue = function () {
+        if (!isNaN(this.nextVal)) {
+            var value = this.nextVal;
+            this.nextVal = NaN;
+            return value;
+        }
+        var resultX, resultY;
+        var isValid = false;
+        while (!isValid) {
+            var v1 = void 0, v2 = void 0, s = void 0;
+            do {
+                v1 = 2 * this.random() - 1;
+                v2 = 2 * this.random() - 1;
+                s = v1 * v1 + v2 * v2;
+            } while (s >= 1 || s === 0);
+            var mul = Math.sqrt(-2.0 * Math.log(s) / s);
+            resultX = this.mean + this.stdDev * v1 * mul;
+            resultY = this.mean + this.stdDev * v2 * mul;
+            if (!this.truncated || this.isValidTruncated(resultX)) {
+                isValid = true;
+            }
+        }
+        if (!this.truncated || this.isValidTruncated(resultY)) {
+            this.nextVal = this.convertValue(resultY);
+        }
+        return this.convertValue(resultX);
+    };
+    MPRandGauss.prototype.convertValue = function (value) {
+        if (this.dtype == null || this.dtype === 'float32') {
+            return value;
+        }
+        return Math.round(value);
+    };
+    MPRandGauss.prototype.isValidTruncated = function (value) {
+        return value <= this.upper && value >= this.lower;
+    };
+    return MPRandGauss;
+}());
+exports.MPRandGauss = MPRandGauss;
+
+},{"seedrandom":2}],97:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.PARALLELIZE_THRESHOLD = 30;
+function computeOptimalWindowSize(inSize) {
+    if (inSize <= exports.PARALLELIZE_THRESHOLD) {
+        return inSize;
+    }
+    return nearestDivisor(inSize, Math.floor(Math.sqrt(inSize)));
+}
+exports.computeOptimalWindowSize = computeOptimalWindowSize;
+function nearestDivisor(size, start) {
+    for (var i = start; i < size; ++i) {
+        if (size % i === 0) {
+            return i;
+        }
+    }
+    return size;
+}
+
+},{}],98:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var util = require("../util");
+function assertParamsValid(input, begin, size) {
+    util.assert(input.rank === begin.length, "Error in slice" + input.rank + "D: Length of begin " + begin + " must " +
+        ("match the rank of the array (" + input.rank + ")."));
+    util.assert(input.rank === size.length, "Error in slice" + input.rank + "D: Length of size " + size + " must " +
+        ("match the rank of the array (" + input.rank + ")."));
+    for (var i = 0; i < input.rank; ++i) {
+        util.assert(begin[i] + size[i] <= input.shape[i], "Error in slice" + input.rank + "D: begin[" + i + "] + size[" + i + "] " +
+            ("(" + (begin[i] + size[i]) + ") would overflow input.shape[" + i + "] (" + input.shape[i] + ")"));
+    }
+}
+exports.assertParamsValid = assertParamsValid;
+
+},{"../util":101}],99:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var SumTypesMap;
+(function (SumTypesMap) {
+    SumTypesMap["float32"] = "float32";
+    SumTypesMap["int32"] = "int32";
+    SumTypesMap["bool"] = "int32";
+})(SumTypesMap = exports.SumTypesMap || (exports.SumTypesMap = {}));
+var UpcastInt32AndMap;
+(function (UpcastInt32AndMap) {
+    UpcastInt32AndMap["float32"] = "float32";
+    UpcastInt32AndMap["int32"] = "int32";
+    UpcastInt32AndMap["bool"] = "int32";
+})(UpcastInt32AndMap = exports.UpcastInt32AndMap || (exports.UpcastInt32AndMap = {}));
+var UpcastBoolAndMap;
+(function (UpcastBoolAndMap) {
+    UpcastBoolAndMap["float32"] = "float32";
+    UpcastBoolAndMap["int32"] = "int32";
+    UpcastBoolAndMap["bool"] = "bool";
+})(UpcastBoolAndMap = exports.UpcastBoolAndMap || (exports.UpcastBoolAndMap = {}));
+var UpcastFloat32AndMap;
+(function (UpcastFloat32AndMap) {
+    UpcastFloat32AndMap["float32"] = "float32";
+    UpcastFloat32AndMap["int32"] = "float32";
+    UpcastFloat32AndMap["bool"] = "float32";
+})(UpcastFloat32AndMap = exports.UpcastFloat32AndMap || (exports.UpcastFloat32AndMap = {}));
+var upcastTypeMap = {
+    float32: UpcastFloat32AndMap,
+    int32: UpcastInt32AndMap,
+    bool: UpcastBoolAndMap
+};
+function upcastType(typeA, typeB) {
+    return upcastTypeMap[typeA][typeB];
+}
+exports.upcastType = upcastType;
+
+},{}],100:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var environment_1 = require("./environment");
+var backend_cpu_1 = require("./math/backends/backend_cpu");
+var backend_webgl_1 = require("./math/backends/backend_webgl");
+var math_1 = require("./math/math");
+var util = require("./util");
+exports.TEST_EPSILON = 1e-2;
+function mean(values) {
+    var sum = 0;
+    for (var i = 0; i < values.length; i++) {
+        sum += values[i];
+    }
+    return sum / values.length;
+}
+exports.mean = mean;
+function standardDeviation(values, mean) {
+    var squareDiffSum = 0;
+    for (var i = 0; i < values.length; i++) {
+        var diff = values[i] - mean;
+        squareDiffSum += diff * diff;
+    }
+    return Math.sqrt(squareDiffSum / values.length);
+}
+exports.standardDeviation = standardDeviation;
+function kurtosis(values) {
+    var valuesMean = mean(values);
+    var n = values.length;
+    var sum2 = 0;
+    var sum4 = 0;
+    for (var i = 0; i < n; i++) {
+        var v = values[i] - valuesMean;
+        sum2 += Math.pow(v, 2);
+        sum4 += Math.pow(v, 4);
+    }
+    return (1 / n) * sum4 / Math.pow((1 / n) * sum2, 2);
+}
+exports.kurtosis = kurtosis;
+function skewness(values) {
+    var valuesMean = mean(values);
+    var n = values.length;
+    var sum2 = 0;
+    var sum3 = 0;
+    for (var i = 0; i < n; i++) {
+        var v = values[i] - valuesMean;
+        sum2 += Math.pow(v, 2);
+        sum3 += Math.pow(v, 3);
+    }
+    return (1 / n) * sum3 / Math.pow((1 / (n - 1)) * sum2, 3 / 2);
+}
+exports.skewness = skewness;
+function jarqueBeraNormalityTest(values) {
+    var n = values.length;
+    var s = skewness(values);
+    var k = kurtosis(values);
+    var jb = n / 6 * (Math.pow(s, 2) + 0.25 * Math.pow(k - 3, 2));
+    var CHI_SQUARE_2DEG = 5.991;
+    if (jb > CHI_SQUARE_2DEG) {
+        throw new Error("Invalid p-value for JB: " + jb);
+    }
+}
+exports.jarqueBeraNormalityTest = jarqueBeraNormalityTest;
+function expectArrayInMeanStdRange(actual, expectedMean, expectedStdDev, epsilon) {
+    if (epsilon === void 0) { epsilon = exports.TEST_EPSILON; }
+    var actualMean = mean(actual);
+    expectNumbersClose(actualMean, expectedMean, epsilon);
+    expectNumbersClose(standardDeviation(actual, actualMean), expectedStdDev, epsilon);
+}
+exports.expectArrayInMeanStdRange = expectArrayInMeanStdRange;
+function expectArraysClose(actual, expected, epsilon) {
+    if (epsilon === void 0) { epsilon = exports.TEST_EPSILON; }
+    var aType = actual.constructor.name;
+    var bType = expected.constructor.name;
+    if (aType !== bType) {
+        throw new Error("Arrays are of different type " + aType + " vs " + bType);
+    }
+    if (actual.length !== expected.length) {
+        throw new Error("Matrices have different lengths (" + actual.length + " vs " +
+            (expected.length + ")."));
+    }
+    for (var i = 0; i < expected.length; ++i) {
+        var a = actual[i];
+        var e = expected[i];
+        if (!areClose(a, e, epsilon)) {
+            var actualStr = "actual[" + i + "] === " + a;
+            var expectedStr = "expected[" + i + "] === " + e;
+            throw new Error('Arrays differ: ' + actualStr + ', ' + expectedStr);
+        }
+    }
+}
+exports.expectArraysClose = expectArraysClose;
+function expectNumbersClose(a, e, epsilon) {
+    if (epsilon === void 0) { epsilon = exports.TEST_EPSILON; }
+    if (!areClose(a, e, epsilon)) {
+        throw new Error("Numbers differ: actual === " + a + ", expected === " + e);
+    }
+}
+exports.expectNumbersClose = expectNumbersClose;
+function areClose(a, e, epsilon) {
+    if (isNaN(a) && isNaN(e)) {
+        return true;
+    }
+    if (isNaN(a) || isNaN(e) || Math.abs(a - e) > epsilon) {
+        return false;
+    }
+    return true;
+}
+function expectValuesInRange(actual, low, high) {
+    for (var i = 0; i < actual.length; i++) {
+        if (actual[i] < low || actual[i] > high) {
+            throw new Error("Value out of range:" + actual[i] + " low: " + low + ", high: " + high);
+        }
+    }
+}
+exports.expectValuesInRange = expectValuesInRange;
+function randomArrayInRange(n, minValue, maxValue) {
+    var v = new Float32Array(n);
+    var range = maxValue - minValue;
+    for (var i = 0; i < n; ++i) {
+        v[i] = (Math.random() * range) + minValue;
+    }
+    return v;
+}
+exports.randomArrayInRange = randomArrayInRange;
+function makeIdentity(n) {
+    var i = new Float32Array(n * n);
+    for (var j = 0; j < n; ++j) {
+        i[(j * n) + j] = 1;
+    }
+    return i;
+}
+exports.makeIdentity = makeIdentity;
+function cpuMultiplyMatrix(a, aRow, aCol, b, bRow, bCol) {
+    var result = new Float32Array(aRow * bCol);
+    for (var r = 0; r < aRow; ++r) {
+        var aOffset = (r * aCol);
+        var cOffset = (r * bCol);
+        for (var c = 0; c < bCol; ++c) {
+            var d = 0;
+            for (var k = 0; k < aCol; ++k) {
+                d += a[aOffset + k] * b[(k * bCol) + c];
+            }
+            result[cOffset + c] = d;
+        }
+    }
+    return result;
+}
+exports.cpuMultiplyMatrix = cpuMultiplyMatrix;
+function cpuDotProduct(a, b) {
+    if (a.length !== b.length) {
+        throw new Error('cpuDotProduct: incompatible vectors.');
+    }
+    var d = 0;
+    for (var i = 0; i < a.length; ++i) {
+        d += a[i] * b[i];
+    }
+    return d;
+}
+exports.cpuDotProduct = cpuDotProduct;
+function describeMathCPU(name, tests, featuresList) {
+    var testNameBase = 'CPU: math.' + name;
+    describeWithFeaturesAndExecutor(testNameBase, tests, function (testName, tests, features) { return executeMathTests(testName, tests, function () {
+        var safeMode = true;
+        return new math_1.NDArrayMath(new backend_cpu_1.MathBackendCPU(), safeMode);
+    }, features); }, featuresList);
+}
+exports.describeMathCPU = describeMathCPU;
+function describeMathGPU(name, tests, featuresList) {
+    var testNameBase = 'WebGL: math.' + name;
+    describeWithFeaturesAndExecutor(testNameBase, tests, function (testName, tests, features) { return executeMathTests(testName, tests, function () {
+        var safeMode = true;
+        return new math_1.NDArrayMath(new backend_webgl_1.MathBackendWebGL(), safeMode);
+    }, features); }, featuresList);
+}
+exports.describeMathGPU = describeMathGPU;
+function describeCustom(name, tests, featuresList, customBeforeEach, customAfterEach) {
+    describeWithFeaturesAndExecutor(name, [tests], function (testName, tests, features) { return executeTests(testName, tests, features, customBeforeEach, customAfterEach); }, featuresList);
+}
+exports.describeCustom = describeCustom;
+function describeWithFeaturesAndExecutor(testNameBase, tests, executor, featuresList) {
+    if (featuresList != null) {
+        featuresList.forEach(function (features) {
+            var testName = testNameBase + ' ' + JSON.stringify(features);
+            executor(testName, tests, features);
+        });
+    }
+    else {
+        executor(testNameBase, tests);
+    }
+}
+var PROMISE_IT = function (name, testFunc) {
+    it(name, function (done) {
+        var result = testFunc();
+        if (result instanceof Promise) {
+            result.then(done, function (e) {
+                fail(e);
+                done();
+            });
+        }
+        else {
+            done();
+        }
+    });
+};
+function executeMathTests(testName, tests, mathFactory, features) {
+    var math;
+    var customBeforeEach = function () {
+        math = mathFactory();
+        environment_1.ENV.setMath(math);
+        math.startScope();
+    };
+    var customAfterEach = function () {
+        math.endScope(null);
+        math.dispose();
+    };
+    var customIt = function (name, testFunc) {
+        PROMISE_IT(name, function () { return testFunc(math); });
+    };
+    executeTests(testName, tests, features, customBeforeEach, customAfterEach, customIt);
+}
+exports.executeMathTests = executeMathTests;
+function executeTests(testName, tests, features, customBeforeEach, customAfterEach, customIt) {
+    if (customIt === void 0) { customIt = PROMISE_IT; }
+    describe(testName, function () {
+        beforeEach(function () {
+            if (features != null) {
+                environment_1.ENV.setFeatures(features);
+                environment_1.ENV.registerBackend('webgl', function () { return new backend_webgl_1.MathBackendWebGL(); });
+                environment_1.ENV.registerBackend('cpu', function () { return new backend_cpu_1.MathBackendCPU(); });
+            }
+            if (customBeforeEach != null) {
+                customBeforeEach();
+            }
+        });
+        afterEach(function () {
+            if (customAfterEach != null) {
+                customAfterEach();
+            }
+            if (features != null) {
+                environment_1.ENV.reset();
+            }
+        });
+        tests.forEach(function (test) { return test(customIt); });
+    });
+}
+exports.executeTests = executeTests;
+function assertIsNan(val, dtype) {
+    if (!util.isValNaN(val, dtype)) {
+        throw new Error("Value " + val + " does not represent NaN for dtype " + dtype);
+    }
+}
+exports.assertIsNan = assertIsNan;
+
+},{"./environment":15,"./math/backends/backend_cpu":55,"./math/backends/backend_webgl":57,"./math/math":94,"./util":101}],101:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+function shuffle(array) {
+    var counter = array.length;
+    var temp = 0;
+    var index = 0;
+    while (counter > 0) {
+        index = (Math.random() * counter) | 0;
+        counter--;
+        temp = array[counter];
+        array[counter] = array[index];
+        array[index] = temp;
+    }
+}
+exports.shuffle = shuffle;
+function clamp(min, x, max) {
+    return Math.max(min, Math.min(x, max));
+}
+exports.clamp = clamp;
+function randUniform(a, b) {
+    return Math.random() * (b - a) + a;
+}
+exports.randUniform = randUniform;
+function distSquared(a, b) {
+    var result = 0;
+    for (var i = 0; i < a.length; i++) {
+        var diff = Number(a[i]) - Number(b[i]);
+        result += diff * diff;
+    }
+    return result;
+}
+exports.distSquared = distSquared;
+function assert(expr, msg) {
+    if (!expr) {
+        throw new Error(msg);
+    }
+}
+exports.assert = assert;
+function assertShapesMatch(shapeA, shapeB, errorMessagePrefix) {
+    if (errorMessagePrefix === void 0) { errorMessagePrefix = ''; }
+    assert(arraysEqual(shapeA, shapeB), errorMessagePrefix + ("Shapes " + shapeA + " and " + shapeB + " must match"));
+}
+exports.assertShapesMatch = assertShapesMatch;
+function flatten(arr, ret) {
+    if (ret === void 0) { ret = []; }
+    if (Array.isArray(arr)) {
+        for (var i = 0; i < arr.length; ++i) {
+            flatten(arr[i], ret);
+        }
+    }
+    else {
+        ret.push(arr);
+    }
+    return ret;
+}
+exports.flatten = flatten;
+function inferShape(arr) {
+    var shape = [];
+    while (arr instanceof Array) {
+        shape.push(arr.length);
+        arr = arr[0];
+    }
+    return shape;
+}
+exports.inferShape = inferShape;
+function sizeFromShape(shape) {
+    if (shape.length === 0) {
+        return 1;
+    }
+    var size = shape[0];
+    for (var i = 1; i < shape.length; i++) {
+        size *= shape[i];
+    }
+    return size;
+}
+exports.sizeFromShape = sizeFromShape;
+function isScalarShape(shape) {
+    return shape.length === 0;
+}
+exports.isScalarShape = isScalarShape;
+function arraysEqual(n1, n2) {
+    if (n1.length !== n2.length) {
+        return false;
+    }
+    for (var i = 0; i < n1.length; i++) {
+        if (n1[i] !== n2[i]) {
+            return false;
+        }
+    }
+    return true;
+}
+exports.arraysEqual = arraysEqual;
+function isInt(a) {
+    return a % 1 === 0;
+}
+exports.isInt = isInt;
+function tanh(x) {
+    if (Math.tanh != null) {
+        return Math.tanh(x);
+    }
+    if (x === Infinity) {
+        return 1;
+    }
+    else if (x === -Infinity) {
+        return -1;
+    }
+    else {
+        var e2x = Math.exp(2 * x);
+        return (e2x - 1) / (e2x + 1);
+    }
+}
+exports.tanh = tanh;
+function sizeToSquarishShape(size) {
+    for (var a = Math.floor(Math.sqrt(size)); a > 1; --a) {
+        if (size % a === 0) {
+            return [a, size / a];
+        }
+    }
+    return [1, size];
+}
+exports.sizeToSquarishShape = sizeToSquarishShape;
+function createShuffledIndices(n) {
+    var shuffledIndices = new Uint32Array(n);
+    for (var i = 0; i < n; ++i) {
+        shuffledIndices[i] = i;
+    }
+    shuffle(shuffledIndices);
+    return shuffledIndices;
+}
+exports.createShuffledIndices = createShuffledIndices;
+function rightPad(a, size) {
+    if (size <= a.length) {
+        return a;
+    }
+    return a + ' '.repeat(size - a.length);
+}
+exports.rightPad = rightPad;
+function repeatedTry(checkFn, delayFn, maxCounter) {
+    if (delayFn === void 0) { delayFn = function (counter) { return 0; }; }
+    return new Promise(function (resolve, reject) {
+        var tryCount = 0;
+        var tryFn = function () {
+            if (checkFn()) {
+                resolve();
+                return;
+            }
+            tryCount++;
+            var nextBackoff = delayFn(tryCount);
+            if (maxCounter != null && tryCount >= maxCounter) {
+                reject();
+                return;
+            }
+            setTimeout(tryFn, nextBackoff);
+        };
+        setTimeout(tryFn, 0);
+    });
+}
+exports.repeatedTry = repeatedTry;
+function getQueryParams(queryString) {
+    var params = {};
+    queryString.replace(/[?&]([^=?&]+)(?:=([^&]*))?/g, function (s) {
+        var t = [];
+        for (var _i = 1; _i < arguments.length; _i++) {
+            t[_i - 1] = arguments[_i];
+        }
+        decodeParam(params, t[0], t[1]);
+        return t.join('=');
+    });
+    return params;
+}
+exports.getQueryParams = getQueryParams;
+function decodeParam(params, name, value) {
+    params[decodeURIComponent(name)] = decodeURIComponent(value || '');
+}
+function inferFromImplicitShape(shape, size) {
+    var shapeProd = 1;
+    var implicitIdx = -1;
+    for (var i = 0; i < shape.length; ++i) {
+        if (shape[i] > 0) {
+            shapeProd *= shape[i];
+        }
+        else if (shape[i] === -1) {
+            if (implicitIdx !== -1) {
+                throw Error("Shapes can only have 1 implicit size. " +
+                    ("Found -1 at dim " + implicitIdx + " and dim " + i));
+            }
+            implicitIdx = i;
+        }
+        else if (shape[i] <= 0) {
+            throw Error("Shapes can not be <= 0. Found " + shape[i] + " at dim " + i);
+        }
+    }
+    if (implicitIdx === -1) {
+        if (size > 0 && size !== shapeProd) {
+            throw Error("Size (" + size + ") must match the product of shape " + shape);
+        }
+        return shape;
+    }
+    if (size % shapeProd !== 0) {
+        throw Error("The implicit shape can't be a fractional number. " +
+            ("Got " + size + " / " + shapeProd));
+    }
+    var newShape = shape.slice();
+    newShape[implicitIdx] = size / shapeProd;
+    return newShape;
+}
+exports.inferFromImplicitShape = inferFromImplicitShape;
+exports.NAN_INT32 = 1 << 31;
+exports.NAN_BOOL = 255;
+exports.NAN_FLOAT32 = NaN;
+function getNaN(dtype) {
+    if (dtype === 'float32') {
+        return exports.NAN_FLOAT32;
+    }
+    else if (dtype === 'int32') {
+        return exports.NAN_INT32;
+    }
+    else if (dtype === 'bool') {
+        return exports.NAN_BOOL;
+    }
+    else {
+        throw new Error("Unknown dtype " + dtype);
+    }
+}
+exports.getNaN = getNaN;
+function isValNaN(val, dtype) {
+    if (isNaN(val)) {
+        return true;
+    }
+    if (dtype === 'float32') {
+        return false;
+    }
+    else if (dtype === 'int32') {
+        return val === exports.NAN_INT32;
+    }
+    else if (dtype === 'bool') {
+        return val === exports.NAN_BOOL;
+    }
+    else {
+        throw new Error("Unknown dtype " + dtype);
+    }
+}
+exports.isValNaN = isValNaN;
+function squeezeShape(shape) {
+    var newShape = [];
+    var keptDims = [];
+    for (var i = 0; i < shape.length; ++i) {
+        if (shape[i] > 1) {
+            newShape.push(shape[i]);
+            keptDims.push(i);
+        }
+    }
+    return { newShape: newShape, keptDims: keptDims };
+}
+exports.squeezeShape = squeezeShape;
+function getTypedArrayFromDType(dtype, size) {
+    var values = null;
+    if (dtype == null || dtype === 'float32') {
+        values = new Float32Array(size);
+    }
+    else if (dtype === 'int32') {
+        values = new Int32Array(size);
+    }
+    else if (dtype === 'bool') {
+        values = new Uint8Array(size);
+    }
+    else {
+        throw new Error("Unknown data type " + dtype);
+    }
+    return values;
+}
+exports.getTypedArrayFromDType = getTypedArrayFromDType;
+
+},{}],102:[function(require,module,exports){
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+var version = '0.3.15';
+exports.version = version;
+
+},{}]},{},[51])(51)
+});
+this.dl = this.deeplearn;
diff --git a/server/static/edges2cats-input.png b/server/static/edges2cats-input.png
index e48918c8523851d6e00d33a785d1cb2709c5447a..9536f954685a79c9d516b973e7fa5a549315d69d 100644
Binary files a/server/static/edges2cats-input.png and b/server/static/edges2cats-input.png differ
diff --git a/server/static/edges2cats-output.png b/server/static/edges2cats-output.png
index b6049e8b609534706dfc53f92d4b268dc2b35a2a..bf747fe0e7e455d016ce5f31034011c2113ec2b1 100644
Binary files a/server/static/edges2cats-output.png and b/server/static/edges2cats-output.png differ
diff --git a/server/static/edges2cats-sheet.jpg b/server/static/edges2cats-sheet.jpg
index 40b8508d3ccb9349a251af8e1a71d554078e1f89..0136f8ac706921d571fbd208a77800b0ce1f5c19 100644
Binary files a/server/static/edges2cats-sheet.jpg and b/server/static/edges2cats-sheet.jpg differ
diff --git a/server/static/edges2handbags-input.png b/server/static/edges2handbags-input.png
deleted file mode 100644
index 8e342bf48c50a7a1ea940846fc53388acb662499..0000000000000000000000000000000000000000
Binary files a/server/static/edges2handbags-input.png and /dev/null differ
diff --git a/server/static/edges2handbags-output.png b/server/static/edges2handbags-output.png
deleted file mode 100644
index 0d4e8330fd1e638d69e4e33c8e66c9ba2882240f..0000000000000000000000000000000000000000
Binary files a/server/static/edges2handbags-output.png and /dev/null differ
diff --git a/server/static/edges2handbags-sheet.jpg b/server/static/edges2handbags-sheet.jpg
deleted file mode 100644
index e06ec790abdaa85c1c75fa75fd702aacf2054dad..0000000000000000000000000000000000000000
Binary files a/server/static/edges2handbags-sheet.jpg and /dev/null differ
diff --git a/server/static/edges2shoes-input.png b/server/static/edges2shoes-input.png
index 296f5b661d9b83c4aa835074b65cde359aa135c0..797ff675bf5b8f050d1d178e971975f90bd2b24b 100644
Binary files a/server/static/edges2shoes-input.png and b/server/static/edges2shoes-input.png differ
diff --git a/server/static/edges2shoes-output.png b/server/static/edges2shoes-output.png
index 4faa4e59c6d982aae2c2466002dbbe569ed23712..3d875725f31d1510f3622469d5c1c7c088467742 100644
Binary files a/server/static/edges2shoes-output.png and b/server/static/edges2shoes-output.png differ
diff --git a/server/static/edges2shoes-sheet.jpg b/server/static/edges2shoes-sheet.jpg
index e535c5ef97d55ec1f0bb2d7732f05d041277ee8f..80e10f6019d236602211644bb0032de46e38af59 100644
Binary files a/server/static/edges2shoes-sheet.jpg and b/server/static/edges2shoes-sheet.jpg differ
diff --git a/server/static/eraser.png b/server/static/eraser.png
new file mode 100644
index 0000000000000000000000000000000000000000..bbfc67fe5dc599fddb2894ccd2111f03e1c26116
Binary files /dev/null and b/server/static/eraser.png differ
diff --git a/server/static/facades-input.png b/server/static/facades-input.png
index f405fc4f8b4ad113199cb067d13064fe1550a649..d8ba69af8c2a931e20c60282a7259320fdbdbda7 100644
Binary files a/server/static/facades-input.png and b/server/static/facades-input.png differ
diff --git a/server/static/facades-output.png b/server/static/facades-output.png
index 03e092b132cd20ac044b2b8262ee7e18fa484f85..dc0353311f10644b8d947197272006c74e6418ad 100644
Binary files a/server/static/facades-output.png and b/server/static/facades-output.png differ
diff --git a/server/static/facades-sheet.jpg b/server/static/facades-sheet.jpg
index e1c4961235fd53a9944112c276953b3535cb6755..5fce2c4283725a6e3646124c00672a6cf98f11d0 100644
Binary files a/server/static/facades-sheet.jpg and b/server/static/facades-sheet.jpg differ
diff --git a/server/static/index.html b/server/static/index.html
index 7e358b3e17db2af3bb9bb7349375068f0a6f9c22..d5b1b321bdabdd40dd9166c75bdb63d0d6038632 100644
--- a/server/static/index.html
+++ b/server/static/index.html
@@ -1,29 +1,29 @@
 <html>
 <body>
 
+<div>edges2cats</div>
+<div id="edges2cats"></div>
+
 <div>edges2shoes</div>
 <div id="edges2shoes"></div>
 
-<div>facades</div>
-<div id="facades"></div>
-
 <div>edges2handbags</div>
 <div id="edges2handbags"></div>
 
-<div>edges2cats</div>
-<div id="edges2cats"></div>
+<div>facades</div>
+<div id="facades"></div>
 
+<script src="deeplearn-0.3.15.js"></script>
 <script>
 
 var editor_background = new Image()
 editor_background.src = "editor.png"
 
+var DEBUG = false
 var SIZE = 256
 
 var editors = []
 var request_in_progress = false
-var last_request_failed = false
-var base_url = ""  // this will cause it to talk to the server of this file
 
 function main() {
   var create_editor = function(config) {
@@ -34,8 +34,8 @@ function main() {
   }
 
   create_editor({
-    name: "edges2shoes",
-    generate_url: base_url + "/edges2shoes_AtoB",
+    name: "edges2cats",
+    weights_url: "/models/edges2cats_AtoB.pict",
     mode: "line",
     clear: "#FFFFFF",
     colors: {
@@ -43,14 +43,14 @@ function main() {
       eraser: "#ffffff",
     },
     draw: "#000000",
-    initial_input: "/edges2shoes-input.png",
-    initial_output: "/edges2shoes-output.png",
-    sheet_url: "/edges2shoes-sheet.jpg",
+    initial_input: "/edges2cats-input.png",
+    initial_output: "/edges2cats-output.png",
+    sheet_url: "/edges2cats-sheet.jpg",
   })
 
   create_editor({
-    name: "edges2handbags",
-    generate_url: base_url + "/edges2handbags_AtoB",
+    name: "edges2shoes",
+    weights_url: "/models/edges2shoes_AtoB.pict",
     mode: "line",
     clear: "#FFFFFF",
     colors: {
@@ -58,14 +58,14 @@ function main() {
       eraser: "#ffffff",
     },
     draw: "#000000",
-    initial_input: "/edges2handbags-input.png",
-    initial_output: "/edges2handbags-output.png",
-    sheet_url: "/edges2handbags-sheet.jpg",
+    initial_input: "/edges2shoes-input.png",
+    initial_output: "/edges2shoes-output.png",
+    sheet_url: "/edges2shoes-sheet.jpg",
   })
 
   create_editor({
-    name: "edges2cats",
-    generate_url: base_url + "/edges2cats_AtoB",
+    name: "edges2handbags",
+    weights_url: "/models/edges2handbags_AtoB.pict",
     mode: "line",
     clear: "#FFFFFF",
     colors: {
@@ -73,14 +73,14 @@ function main() {
       eraser: "#ffffff",
     },
     draw: "#000000",
-    initial_input: "/edges2cats-input.png",
-    initial_output: "/edges2cats-output.png",
-    sheet_url: "/edges2cats-sheet.jpg",
+    initial_input: "/edges2handbags-input.png",
+    initial_output: "/edges2handbags-output.png",
+    sheet_url: "/edges2handbags-sheet.jpg",
   })
 
   create_editor({
     name: "facades",
-    generate_url: base_url + "/facades_BtoA",
+    weights_url: "/models/facades_BtoA.pict",
     mode: "rect",
     colors: {
       background: "#0006d9",
@@ -89,7 +89,7 @@ function main() {
       "window": "#0075ff",
       "window sill": "#68f898",
       "window head": "#1dffdd",
-      "shutter": "#eeed28",
+      shutter: "#eeed28",
       balcony: "#b8ff38",
       trim: "#ff9204",
       cornice: "#ff4401",
@@ -103,7 +103,7 @@ function main() {
     sheet_url: "/facades-sheet.jpg",
   })
 
-  init()
+  window.requestAnimationFrame(frame)
 }
 window.onload = main
 
@@ -113,6 +113,156 @@ function render() {
   }
 }
 
+
+// model
+
+var weights_cache = {}
+function fetch_weights(path, progress_cb) {
+  return new Promise(function(resolve, reject) {
+    if (path in weights_cache) {
+      resolve(weights_cache[path])
+      return
+    }
+
+    var xhr = new XMLHttpRequest()
+    xhr.open("GET", path, true)
+    xhr.responseType = "arraybuffer"
+
+    xhr.onprogress = function(e) {
+      progress_cb(e.loaded, e.total)
+    }
+
+    xhr.onload = function(e) {
+      if (xhr.status != 200) {
+        reject("missing model")
+        return
+      }
+      var buf = xhr.response
+      if (!buf) {
+        reject("invalid arraybuffer")
+        return
+      }
+
+      var parts = []
+      var offset = 0
+      while (offset < buf.byteLength) {
+        var b = new Uint8Array(buf.slice(offset, offset+4))
+        offset += 4
+        var len = (b[0] << 24) + (b[1] << 16) + (b[2] << 8) + b[3]
+        parts.push(buf.slice(offset, offset + len))
+        offset += len
+      }
+
+      var shapes = JSON.parse((new TextDecoder("utf8")).decode(parts[0]))
+      var index = new Float32Array(parts[1])
+      var encoded = new Uint8Array(parts[2])
+
+      // decode using index
+      var arr = new Float32Array(encoded.length)
+      for (var i = 0; i < arr.length; i++) {
+        arr[i] = index[encoded[i]]
+      }
+
+      var weights = {}
+      var offset = 0
+      for (var i = 0; i < shapes.length; i++) {
+        var shape = shapes[i].shape
+        var size = shape.reduce((total, num) => total * num)
+        var values = arr.slice(offset, offset+size)
+        var dlarr = dl.Array1D.new(values, "float32")
+        weights[shapes[i].name] = dlarr.reshape(shape)
+        offset += size
+      }
+      weights_cache[path] = weights
+      resolve(weights)
+    }
+    xhr.send(null)
+  })
+}
+
+function model(input, weights) {
+  const math = dl.ENV.math
+
+  function preprocess(input) {
+    return math.subtract(math.multiply(input, dl.Scalar.new(2)), dl.Scalar.new(1))
+  }
+
+  function deprocess(input) {
+    return math.divide(math.add(input, dl.Scalar.new(1)), dl.Scalar.new(2))
+  }
+
+  function batchnorm(input, scale, offset) {
+    var moments = math.moments(input, [0, 1])
+    const varianceEpsilon = 1e-5
+    return math.batchNormalization3D(input, moments.mean, moments.variance, varianceEpsilon, scale, offset)
+  }
+
+  function conv2d(input, filter, bias) {
+    return math.conv2d(input, filter, bias, [2, 2], "same")
+  }
+
+  function deconv2d(input, filter, bias) {
+    var convolved = math.conv2dTranspose(input, filter, [input.shape[0]*2, input.shape[1]*2, filter.shape[2]], [2, 2], "same")
+    var biased = math.add(convolved, bias)
+    return biased
+  }
+
+  var preprocessed_input = preprocess(input)
+
+  var layers = []
+
+  var filter = weights["generator/encoder_1/conv2d/kernel"]
+  var bias = weights["generator/encoder_1/conv2d/bias"]
+  var convolved = conv2d(preprocessed_input, filter, bias)
+  layers.push(convolved)
+
+  for (var i = 2; i <= 8; i++) {
+    var scope = "generator/encoder_" + i.toString()
+    var filter = weights[scope + "/conv2d/kernel"]
+    var bias = weights[scope + "/conv2d/bias"]
+    var layer_input = layers[layers.length - 1]
+    var rectified = math.leakyRelu(layer_input, 0.2)
+    var convolved = conv2d(rectified, filter, bias)
+    var scale = weights[scope + "/batch_normalization/gamma"]
+    var offset = weights[scope + "/batch_normalization/beta"]
+    var normalized = batchnorm(convolved, scale, offset)
+    layers.push(normalized)
+  }
+
+  for (var i = 8; i >= 2; i--) {
+    if (i == 8) {
+      var layer_input = layers[layers.length - 1]
+    } else {
+      var skip_layer = i - 1
+      var layer_input = math.concat3D(layers[layers.length - 1], layers[skip_layer], 2)
+    }
+    var rectified = math.relu(layer_input)
+    var scope = "generator/decoder_" + i.toString()
+    var filter = weights[scope + "/conv2d_transpose/kernel"]
+    var bias = weights[scope + "/conv2d_transpose/bias"]
+    var convolved = deconv2d(rectified, filter, bias)
+    var scale = weights[scope + "/batch_normalization/gamma"]
+    var offset = weights[scope + "/batch_normalization/beta"]
+    var normalized = batchnorm(convolved, scale, offset)
+    // missing dropout
+    layers.push(normalized)
+  }
+
+  var layer_input = math.concat3D(layers[layers.length - 1], layers[0], 2)
+  var rectified = math.relu(layer_input)
+  var filter = weights["generator/decoder_1/conv2d_transpose/kernel"]
+  var bias = weights["generator/decoder_1/conv2d_transpose/bias"]
+  var convolved = deconv2d(rectified, filter, bias)
+  var rectified = math.tanh(convolved)
+  layers.push(rectified)
+
+  var output = layers[layers.length - 1]
+  var deprocessed_output = deprocess(output)
+
+  return deprocessed_output
+}
+
+
 // editor
 
 function Editor(config) {
@@ -138,6 +288,9 @@ function Editor(config) {
     this.output.drawImage(output, 0, 0)
   }
 
+  this.progress = null
+  this.last_failure = null
+
   this.sheet_loaded = false
   this.sheet = new Image()
   this.sheet.src = this.config.sheet_url
@@ -164,7 +317,7 @@ Editor.prototype = {
     }
     this.buffer = this.buffers.pop()
   },
-render: function() {
+  render: function() {
     var v = this.view
 
     v.ctx.clearRect(0, 0, v.f.width, v.f.height)
@@ -181,7 +334,7 @@ render: function() {
           if (v.contains(mouse_pos)) {
             cursor_style = "pointer"
           }
-          
+
           if (mouse_released && v.contains(mouse_pos)) {
             this.config.draw = color
             update()
@@ -275,7 +428,7 @@ render: function() {
             if (mouse_down) {
               v.ctx.save()
               v.ctx.rect(0, 0, v.f.width, v.f.height)
-              v.ctx.clip();
+              v.ctx.clip()
               v.ctx.fillStyle = this.config.draw
               v.ctx.fillRect(start.x, start.y, width, height)
               v.ctx.restore()
@@ -291,47 +444,73 @@ render: function() {
     })
 
     v.frame("process_button", 461 - 32, 148, 32*2, 40, () => {
-      if (request_in_progress) {
-        do_button(v, "...")
+      if (this.progress != null) {
+        v.ctx.font = "12px Arial"
+        v.ctx.fillStyle = "#000"
+        var s = "downloading"
+        v.ctx.fillText(s, (v.f.width - v.ctx.measureText(s).width)/2, 5)
+        s = "model"
+        v.ctx.fillText(s, (v.f.width - v.ctx.measureText(s).width)/2, 15)
+
+        v.frame("progress_bar", 0, 25, v.f.width, 15, () => {
+          v.ctx.fillStyle = "#f92672"
+          v.ctx.fillRect(0, 0, v.f.width * this.progress, v.f.height)
+        })
+      } else if (request_in_progress) {
+        do_button(v, "running")
       } else {
         if (do_button(v, "process")) {
           if (request_in_progress) {
             console.log("request already in progress")
             return
           }
+          request_in_progress = true
+          this.last_failure = null
 
-          last_request_failed = false
-          var convert = createContext(SIZE, SIZE, 1)
-          convert.drawImage(this.buffer.canvas, 0, 0, convert.canvas.width, convert.canvas.height)
-          var input_b64 = convert.canvas.toDataURL("image/png").replace(/^data:image\/png;base64,/, "")
-          var xhr = new XMLHttpRequest()
-          xhr.open("POST", this.config.generate_url, true)
-          xhr.setRequestHeader("Content-Type", "image/png")
-          xhr.responseType = "arraybuffer"
-          xhr.timeout = 45000
-
-          xhr.onreadystatechange = () => {
-          	if (xhr.readyState == 4) {
-              request_in_progress = false
-              update()
-              if (xhr.status == 200) {
-                var output_bin = new Uint8Array(xhr.response)
-                var output_b64 = bin_to_b64(output_bin)
-                var output = new Image()
-                output.src = "data:image\/png;base64," + output_b64
-                output.onload = () => {
-                  // browsers besides chrome need to wait for the image to load
-                  this.output.drawImage(output, 0, 0)
-                  update()
-                }
-              } else {
-                last_request_failed = true
-              }
-          	}
+          this.progress = 0
+          progress_cb = (retrieved, total) => {
+            this.progress = retrieved/total
+            update()
           }
-          request_in_progress = true
-          update()
-          xhr.send(b64_to_bin(input_b64))
+
+          fetch_weights(this.config.weights_url, progress_cb).then((weights) => {
+            this.progress = null
+            update()
+            // delay a short period of time so that UI updates before the model uses all the CPU
+            delay(() => {
+              // var g = new dl.Graph()
+
+              var convert = createContext(SIZE, SIZE, 1)
+              convert.drawImage(this.buffer.canvas, 0, 0, convert.canvas.width, convert.canvas.height)
+              var input_uint8_data = convert.getImageData(0, 0, SIZE, SIZE).data
+              var input_float32_data = Float32Array.from(input_uint8_data, (x) => x / 255)
+
+              console.time('render')
+              const math = dl.ENV.math
+              math.startScope()
+              var input_rgba = dl.Array3D.new([SIZE, SIZE, 4], input_float32_data, "float32")
+              var input_rgb = math.slice3D(input_rgba, [0, 0, 0], [SIZE, SIZE, 3])
+
+              var output_rgb = model(input_rgb, weights)
+
+              var alpha = dl.Array3D.ones([SIZE, SIZE, 1])
+              var output_rgba = math.concat3D(output_rgb, alpha, 2)
+
+              output_rgba.getValuesAsync().then((output_float32_data) => {
+                var output_uint8_data = Uint8ClampedArray.from(output_float32_data, (x) => x * 255)
+                this.output.putImageData(new ImageData(output_uint8_data, SIZE, SIZE), 0, 0)
+                math.endScope()
+                console.timeEnd('render')
+                request_in_progress = false
+                update()
+              })
+            })
+          }, (e) => {
+            this.last_failure = e
+            this.progress = null
+            request_in_progress = false
+            update()
+          })
         }
       }
     })
@@ -384,19 +563,19 @@ render: function() {
         a.href = url
         a.download = "pix2pix.png"
         // use createEvent instead of .click() to work in firefox
-        // also can't revoke the object url because firefox breaks
+        // also can"t revoke the object url because firefox breaks
         var event = document.createEvent("MouseEvents")
         event.initEvent("click", true, true)
         a.dispatchEvent(event)
-        // safari doesn't work at all
+        // safari doesn"t work at all
       }
     })
 
-    if (last_request_failed) {
+    if (this.last_failure != null) {
       v.frame("server_error", 50, 350, v.f.width, 50, () => {
         v.ctx.font = "20px Arial"
         v.ctx.fillStyle = "red"
-        v.center_text("error connecting to server, try again later")
+        v.center_text(fmt("error %s", this.last_failure))
       })
     }
   },
@@ -427,17 +606,128 @@ function b64_to_bin(str) {
   return bin
 }
 
-function bin_to_b64(bin) {
-  var parts = []
-  for (var i = 0; i < bin.length; i++) {
-    parts.push(String.fromCharCode(bin[i]))
+function delay(fn) {
+  setTimeout(fn, 0)
+}
+
+function default_format(obj) {
+  if (typeof(obj) === "string") {
+    return obj
+  } else {
+    return JSON.stringify(obj)
   }
-  var binstr = parts.join("")
-  return btoa(binstr)
 }
 
+function fmt() {
+  if (arguments.length === 0) {
+    return "error"
+  }
+
+  var format = arguments[0]
+  var output = ""
+
+  var arg_index = 1
+  var i = 0
+
+  while (i < format.length) {
+    var c = format[i]
+    i++
+
+    if (c != "%") {
+      output += c
+      continue
+    }
+
+    if (i === format.length) {
+      output += "%!(NOVERB)"
+      break
+    }
 
-// immediate mode
+    var flag = format[i]
+    i++
+
+    var pad_char = " "
+
+    if (flag == "0") {
+      pad_char = "0"
+    } else {
+      // not a flag
+      i--
+    }
+
+    var width = 0
+    while (format[i] >= "0" && format[i] <= "9") {
+      width *= 10
+      width += parseInt(format[i], 10)
+      i++
+    }
+
+    var f = format[i]
+    i++
+
+    if (f === "%") {
+      output += "%"
+      continue
+    }
+
+    if (arg_index === arguments.length) {
+      output += "%!" + f + "(MISSING)"
+      continue
+    }
+
+    var arg = arguments[arg_index]
+    arg_index++
+
+    var o = null
+
+    if (f === "v") {
+      o = default_format(arg)
+    } else if (f === "s" && typeof(arg) === "string") {
+      o = arg
+    } else if (f === "T") {
+      o = typeof(arg)
+    } else if (f === "d" && typeof(arg) === "number") {
+      o = arg.toFixed(0)
+    } else if (f === "f" && typeof(arg) === "number") {
+      o = arg.toString()
+    } else if (f === "x" && typeof(arg) === "number") {
+      o = Math.round(arg).toString(16)
+    } else if (f === "t" && typeof(arg) === "boolean") {
+      if (arg) {
+        o = "true"
+      } else {
+        o = "false"
+      }
+    } else {
+      output += "%!" + f + "(" + typeof(arg) + "=" + default_format(arg) + ")"
+    }
+
+    if (o !== null) {
+      if (o.length < width) {
+        output += Array(width - o.length + 1).join(pad_char)
+      }
+      output += o
+    }
+  }
+
+  if (arg_index < arguments.length) {
+    output += "%!(EXTRA "
+    while (arg_index < arguments.length) {
+      var arg = arguments[arg_index]
+      output += typeof(arg) + "=" + default_format(arg)
+      if (arg_index < arguments.length - 1) {
+        output += ", "
+      }
+      arg_index++
+    }
+    output += ")"
+  }
+
+  return output
+}
+
+
+// immediate mode UI
 
 var SCALE = 2
 
@@ -456,6 +746,32 @@ var mouse_down = false
 var mouse_pressed = false
 var mouse_released = false
 
+if (DEBUG) {
+  var fps_elem = document.createElement("div")
+  stylize(fps_elem, {
+    width: "300px",
+    height: "20px",
+    margin: "5px",
+    fontFamily: "Monaco",
+    fontSize: "12px",
+    position: "absolute",
+    top: fmt("%dpx", 10),
+    right: fmt("%dpx", 10),
+  })
+  document.body.insertBefore(fps_elem, document.body.firstChild)
+
+  var status_elem = document.createElement("div")
+  stylize(status_elem, {
+    width: "10px",
+    height: "10px",
+    margin: "5px",
+    position: "absolute",
+    top: fmt("%dpx", 10),
+    left: fmt("%dpx", 10),
+  })
+  document.body.insertBefore(status_elem, document.body.firstChild)
+}
+
 function View(name, width, height) {
   this.ctx = createContext(width, height, SCALE)
 	// https://developer.apple.com/library/safari/documentation/AudioVideo/Conceptual/HTML-canvas-guide/AddingText/AddingText.html
@@ -574,8 +890,14 @@ function frame() {
   var raf = window.requestAnimationFrame(frame)
 
   if (!updated && Object.keys(animations).length == 0) {
+    if (DEBUG) {
+      status_elem.style.backgroundColor = "black"
+    }
     return
   }
+  if (DEBUG) {
+    status_elem.style.backgroundColor = "red"
+  }
 
   now = new Date()
   cursor_style = null
@@ -594,6 +916,13 @@ function frame() {
     document.body.style.cursor = cursor_style
   }
 
+  if (DEBUG) {
+    var decay = 0.9
+    var current_frame_rate = 1 / ((now - last_frame) / 1000)
+    frame_rate = frame_rate * decay + current_frame_rate * (1 - decay)
+    fps_elem.textContent = fmt("fps: %d", frame_rate)
+  }
+
   last_frame = now
   last_mouse_pos = mouse_pos
   mouse_pressed = false
@@ -689,126 +1018,6 @@ document.addEventListener("mouseup", function(e) {
   update()
 })
 
-function default_format(obj) {
-	if (typeof(obj) === "string") {
-		return obj
-	} else {
-		return JSON.stringify(obj)
-	}
-}
-
-function fmt() {
-	if (arguments.length === 0) {
-		return "error"
-	}
-
-	var format = arguments[0]
-	var output = ""
-
-	var arg_index = 1
-	var i = 0
-
-	while (i < format.length) {
-		var c = format[i]
-		i++
-
-		if (c != "%") {
-			output += c
-			continue
-		}
-
-		if (i === format.length) {
-			output += "%!(NOVERB)"
-			break
-		}
-
-		var flag = format[i]
-		i++
-
-		var pad_char = " "
-
-		if (flag == "0") {
-			pad_char = "0"
-		} else {
-			// not a flag
-			i--
-		}
-
-		var width = 0
-		while (format[i] >= "0" && format[i] <= "9") {
-			width *= 10
-			width += parseInt(format[i], 10)
-			i++
-		}
-
-		var f = format[i]
-		i++
-
-		if (f === "%") {
-			output += "%"
-			continue
-		}
-
-		if (arg_index === arguments.length) {
-			output += "%!" + f + "(MISSING)"
-			continue
-		}
-
-		var arg = arguments[arg_index]
-		arg_index++
-
-		var o = null
-
-		if (f === "v") {
-			o = default_format(arg)
-		} else if (f === "s" && typeof(arg) === "string") {
-			o = arg
-		} else if (f === "T") {
-			o = typeof(arg)
-		} else if (f === "d" && typeof(arg) === "number") {
-			o = arg.toFixed(0)
-		} else if (f === "f" && typeof(arg) === "number") {
-			o = arg.toString()
-		} else if (f === "x" && typeof(arg) === "number") {
-			o = Math.round(arg).toString(16)
-		} else if (f === "t" && typeof(arg) === "boolean") {
-			if (arg) {
-				o = "true"
-			} else {
-				o = "false"
-			}
-		} else {
-			output += "%!" + f + "(" + typeof(arg) + "=" + default_format(arg) + ")"
-		}
-
-		if (o !== null) {
-			if (o.length < width) {
-				output += Array(width - o.length + 1).join(pad_char)
-			}
-			output += o
-		}
-	}
-
-	if (arg_index < arguments.length) {
-		output += "%!(EXTRA "
-		while (arg_index < arguments.length) {
-			var arg = arguments[arg_index]
-			output += typeof(arg) + "=" + default_format(arg)
-			if (arg_index < arguments.length - 1) {
-				output += ", "
-			}
-			arg_index++
-		}
-		output += ")"
-	}
-
-	return output
-}
-
-function init() {
-  window.requestAnimationFrame(frame)
-}
-
 </script>
 
 </body>
diff --git a/server/terraform.tfvars.example b/server/terraform.tfvars.example
deleted file mode 100644
index dc870d733f2c360bb9ccbfec30e286dce809bd6c..0000000000000000000000000000000000000000
--- a/server/terraform.tfvars.example
+++ /dev/null
@@ -1,5 +0,0 @@
-google_project = "example"
-
-google_credentials_file = "service-account.json"
-
-server_image_version = "v1"
\ No newline at end of file
diff --git a/server/tools/dump_checkpoints/checkpoint_dumper.py b/server/tools/dump_checkpoints/checkpoint_dumper.py
new file mode 100644
index 0000000000000000000000000000000000000000..6db211d41daa66489d02d9bc38243bea601a2296
--- /dev/null
+++ b/server/tools/dump_checkpoints/checkpoint_dumper.py
@@ -0,0 +1,138 @@
+# Copyright 2017 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==============================================================================
+
+"""This script defines CheckpointDumper class.
+
+This class serves as a base class for other deeplearning checkpoint dumper
+classes and defines common methods, attributes etc.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import json
+import os
+import re
+import string
+
+class CheckpointDumper(object):
+
+  """Base Checkpoint Dumper class.
+
+  Attributes
+  ----------
+  checkpoint_file : str
+      Path to the model checkpoint
+  FILENAME_CHARS : str
+      Allowed file char names
+  manifest : dict
+      Manifest file defining variables
+  output_dir : str
+      Output directory path
+  remove_variables_regex : str
+      Regex expression for variables to be ignored
+  remove_variables_regex_re : sre.SRE_Pattern
+      Compiled `remove variable` regex
+  """
+  
+  FILENAME_CHARS = string.ascii_letters + string.digits + '_'
+
+  def __init__(self, checkpoint_file, output_dir, remove_variables_regex):
+    """Constructs object for Checkpoint Dumper.
+
+    Parameters
+    ----------
+    checkpoint_file : str
+        Path to the model checkpoint
+    output_dir : str
+        Output directory path
+    remove_variables_regex : str
+        Regex expression for variables to be ignored
+    """
+    self.checkpoint_file = os.path.expanduser(checkpoint_file)
+    self.output_dir = os.path.expanduser(output_dir)
+    self.remove_variables_regex = remove_variables_regex
+
+    self.manifest = {}
+    self.remove_variables_regex_re = re.compile(self.remove_variables_regex)
+
+    self.make_dir(self.output_dir)
+
+
+  @staticmethod
+  def make_dir(directory):
+    """Makes directory if not existing.
+    
+    Parameters
+    ----------
+    directory : str
+        Path to directory
+    """
+    if not os.path.exists(directory):
+      os.makedirs(directory)
+
+
+  def should_ignore(self, name):
+    """Checks whether name should be ignored or not.
+
+    Parameters
+    ----------
+    name : str
+        Name to be checked
+
+    Returns
+    -------
+    bool
+        Whether to ignore the name or not
+    """
+    return self.remove_variables_regex and re.match(self.remove_variables_regex_re, name)
+
+
+  def dump_weights(self, variable_name, filename, shape, weights):
+    """Creates a file with given name and dumps byte weights in it.
+
+    Parameters
+    ----------
+    variable_name : str
+        Name of given variable
+    filename : str
+        File name for given variable
+    shape : list
+        Shape of given variable
+    weights : ndarray
+        Weights for given variable
+    """
+    self.manifest[variable_name] = {'filename': filename, 'shape': shape}
+
+    print('Writing variable ' + variable_name + '...')
+    with open(os.path.join(self.output_dir, filename), 'wb') as f:
+      f.write(weights.tobytes())
+
+
+  def dump_manifest(self, filename='manifest.json'):
+    """Creates a manifest file with given name and dumps meta information
+    related to model.
+
+    Parameters
+    ----------
+    filename : str, optional
+        Manifest file name
+    """
+    manifest_fpath = os.path.join(self.output_dir, filename)
+
+    print('Writing manifest to ' + manifest_fpath)
+    with open(manifest_fpath, 'w') as f:
+      f.write(json.dumps(self.manifest, indent=2, sort_keys=True))
diff --git a/server/tools/dump_checkpoints/dump_checkpoint_vars.py b/server/tools/dump_checkpoints/dump_checkpoint_vars.py
new file mode 100644
index 0000000000000000000000000000000000000000..87f999f617e97741f54814d7431cc61d4ce97a5d
--- /dev/null
+++ b/server/tools/dump_checkpoints/dump_checkpoint_vars.py
@@ -0,0 +1,95 @@
+# Copyright 2017 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==============================================================================
+
+"""
+This script is an entry point for dumping checkpoints for various deeplearning
+frameworks.
+"""
+from __future__ import print_function
+
+import argparse
+
+
+def get_checkpoint_dumper(model_type, checkpoint_file, output_dir, remove_variables_regex):
+  """Returns Checkpoint dumper instance for a given model type.
+
+  Parameters
+  ----------
+  model_type : str
+      Type of deeplearning framework
+  checkpoint_file : str
+      Path to checkpoint file
+  output_dir : str
+      Path to output directory
+  remove_variables_regex : str
+      Regex for variables to be ignored
+
+  Returns
+  -------
+  (TensorflowCheckpointDumper, PytorchCheckpointDumper)
+      Checkpoint Dumper Instance for corresponding model type
+
+  Raises
+  ------
+  Error
+      If particular model type is not supported
+  """
+  if model_type == 'tensorflow':
+    from tensorflow_checkpoint_dumper import TensorflowCheckpointDumper
+
+    return TensorflowCheckpointDumper(
+      checkpoint_file, output_dir, remove_variables_regex)
+  elif model_type == 'pytorch':
+    from pytorch_checkpoint_dumper import PytorchCheckpointDumper
+
+    return PytorchCheckpointDumper(
+      checkpoint_file, output_dir, remove_variables_regex)
+  else:
+    raise Error('Currently, "%s" models are not supported'.format(model_type))
+
+
+if __name__ == '__main__':
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--model_type',
+      type=str,
+      required=True,
+      help='Model checkpoint type')
+  parser.add_argument(
+      '--checkpoint_file',
+      type=str,
+      required=True,
+      help='Path to the model checkpoint')
+  parser.add_argument(
+      '--output_dir',
+      type=str,
+      required=True,
+      help='The output directory where to store the converted weights')
+  parser.add_argument(
+      '--remove_variables_regex',
+      type=str,
+      default='',
+      help='A regular expression to match against variable names that should '
+      'not be included')
+  FLAGS, unparsed = parser.parse_known_args()
+
+  if unparsed:
+    parser.print_help()
+    print('Unrecognized flags: ', unparsed)
+    exit(-1)
+
+  checkpoint_dumper = get_checkpoint_dumper(
+    FLAGS.model_type, FLAGS.checkpoint_file, FLAGS.output_dir, FLAGS.remove_variables_regex)
+  checkpoint_dumper.build_and_dump_vars()
diff --git a/server/tools/dump_checkpoints/pytorch_checkpoint_dumper.py b/server/tools/dump_checkpoints/pytorch_checkpoint_dumper.py
new file mode 100644
index 0000000000000000000000000000000000000000..e3568da20e23519d943c03bb0d1e7bd268a0059c
--- /dev/null
+++ b/server/tools/dump_checkpoints/pytorch_checkpoint_dumper.py
@@ -0,0 +1,104 @@
+# Copyright 2017 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==============================================================================
+
+"""This script defines PytorchCheckpointDumper class.
+
+This class takes a pytorch checkpoint file and writes all of the variables in the
+checkpoint to a directory which deeplearnjs can take as input.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from six import iteritems
+
+import argparse
+import os
+import re
+import json
+import string
+
+import numpy as np
+
+import torch
+
+from checkpoint_dumper import CheckpointDumper
+
+class PytorchCheckpointDumper(CheckpointDumper):
+
+  """Class for dumping Pytorch Checkpoints.
+
+  Attributes
+  ----------
+  state_dictionary : dict
+      Dictionary defining checkpoint variables and weights
+  """
+
+  def __init__(self, checkpoint_file, output_dir, remove_variables_regex):
+    """Constructs object for Pytorch Checkpoint Dumper.
+
+    Parameters
+    ----------
+    checkpoint_file : str
+        Path to the model checkpoint
+    output_dir : str
+        Output directory path
+    remove_variables_regex : str
+        Regex expression for variables to be ignored
+    """
+    super(PytorchCheckpointDumper, self).__init__(
+      checkpoint_file, output_dir, remove_variables_regex)
+
+    self.state_dictionary = torch.load(self.checkpoint_file)
+
+  def var_name_to_filename(self, var_name):
+    """Converts variable names to standard file names.
+
+    Parameters
+    ----------
+    var_name : str
+        Variable name to be converted
+
+    Returns
+    -------
+    str
+        Standardized file name
+    """
+    chars = []
+
+    for c in var_name:
+      if c in CheckpointDumper.FILENAME_CHARS:
+        chars.append(c)
+      elif c == '.':
+        chars.append('_')
+
+    return ''.join(chars)
+
+  def build_and_dump_vars(self):
+    """Builds and dumps variables and a manifest file.
+    """
+    for (var_name, var_weights) in iteritems(self.state_dictionary):
+      if (self.should_ignore(var_name)):
+        print('Ignoring ' + var_name)
+        continue
+
+      var_filename = self.var_name_to_filename(var_name)
+      var_shape = list(map(int, list(var_weights.size())))
+      tensor = var_weights.cpu().numpy()
+
+      self.dump_weights(var_name, var_filename, var_shape, tensor)
+
+    self.dump_manifest()
diff --git a/server/tools/dump_checkpoints/tensorflow_checkpoint_dumper.py b/server/tools/dump_checkpoints/tensorflow_checkpoint_dumper.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c95ace1e2f5386db80375d9088b7a3c285972e8
--- /dev/null
+++ b/server/tools/dump_checkpoints/tensorflow_checkpoint_dumper.py
@@ -0,0 +1,103 @@
+# Copyright 2017 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+# ==============================================================================
+
+"""This script defines TensorflowCheckpointDumper class.
+
+This class takes a tensorflow checkpoint file and writes all of the variables in the
+checkpoint to a directory which deeplearnjs can take as input.
+"""
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+from six import iteritems
+
+import argparse
+import json
+import os
+import re
+
+import tensorflow as tf
+
+from checkpoint_dumper import CheckpointDumper
+
+class TensorflowCheckpointDumper(CheckpointDumper):
+
+  """Class for dumping Tensorflow Checkpoints.
+
+  Attributes
+  ----------
+  reader : NewCheckpointReader
+      Reader for given tensorflow checkpoint
+  """
+
+  def __init__(self, checkpoint_file, output_dir, remove_variables_regex):
+    """Constructs object for Tensorflow Checkpoint Dumper.
+
+    Parameters
+    ----------
+    checkpoint_file : str
+        Path to the model checkpoint
+    output_dir : str
+        Output directory path
+    remove_variables_regex : str
+        Regex expression for variables to be ignored
+    """
+    super(TensorflowCheckpointDumper, self).__init__(
+      checkpoint_file, output_dir, remove_variables_regex)
+
+    self.reader = tf.train.NewCheckpointReader(self.checkpoint_file)
+
+  def var_name_to_filename(self, var_name):
+    """Converts variable names to standard file names.
+
+    Parameters
+    ----------
+    var_name : str
+        Variable name to be converted
+
+    Returns
+    -------
+    str
+        Standardized file name
+    """
+    chars = []
+
+    for c in var_name:
+      if c in CheckpointDumper.FILENAME_CHARS:
+        chars.append(c)
+      elif c == '/':
+        chars.append('_')
+
+    return ''.join(chars)
+
+  def build_and_dump_vars(self):
+    """Builds and dumps variables and a manifest file.
+    """
+    var_to_shape_map = self.reader.get_variable_to_shape_map()
+
+    for (var_name, var_shape) in iteritems(var_to_shape_map):
+      if self.should_ignore(var_name) or var_name == 'global_step':
+        print('Ignoring ' + var_name)
+        continue
+
+      var_filename = self.var_name_to_filename(var_name)
+      self.manifest[var_name] = {'filename': var_filename, 'shape': var_shape}
+
+      tensor = self.reader.get_tensor(var_name)
+      self.dump_weights(var_name, var_filename, var_shape, tensor)
+
+    self.dump_manifest()
diff --git a/server/tools/export-checkpoint.py b/server/tools/export-checkpoint.py
new file mode 100644
index 0000000000000000000000000000000000000000..253e421286d8aff82ec4519d2ef7e50e03a9f8ab
--- /dev/null
+++ b/server/tools/export-checkpoint.py
@@ -0,0 +1,101 @@
+import argparse
+import os
+import tempfile
+import subprocess as sp
+import json
+import struct
+import time
+
+import numpy as np
+
+SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
+
+
+def log_quantize(data, mu, bins):
+    # mu-law encoding
+    scale = np.max(np.abs(data))
+    norm_data = data / scale
+    log_data = np.sign(data) * np.log(1 + mu * np.abs(norm_data)) / np.log(1 + mu)
+
+    _counts, edges = np.histogram(log_data, bins=bins)
+    log_points = (edges[:-1] + edges[1:]) / 2
+    return np.sign(log_points) * (1 / mu) * ((1 + mu)**np.abs(log_points) - 1) * scale
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--checkpoint", required=True, help="directory with checkpoint to resume training from or use for testing")
+    parser.add_argument("--output_file", required=True, help="where to write output")
+    args = parser.parse_args()
+
+    model_path = None
+    with open(os.path.join(args.checkpoint, "checkpoint")) as f:
+        for line in f:
+            line = line.strip()
+            if line == "":
+                continue
+            key, _sep, val = line.partition(": ")
+            val = val[1:-1]  # remove quotes
+            if key == "model_checkpoint_path":
+                model_path = val
+
+    if model_path is None:
+        raise Exception("failed to find model path")
+
+    checkpoint_file = os.path.join(args.checkpoint, model_path)
+    with tempfile.TemporaryDirectory() as tmp_dir:
+        cmd = ["python", "-u", os.path.join(SCRIPT_DIR, "dump_checkpoints/dump_checkpoint_vars.py"), "--model_type", "tensorflow", "--output_dir", tmp_dir, "--checkpoint_file", checkpoint_file]
+        sp.check_call(cmd)
+
+        with open(os.path.join(tmp_dir, "manifest.json")) as f:
+            manifest = json.loads(f.read())
+
+        names = []
+        for key in manifest.keys():
+            if not key.startswith("generator") or "Adam" in key or "_loss" in key or "_train" in key or "_moving_" in key:
+                continue
+            names.append(key)
+        names = sorted(names)
+
+        arrays = []
+        for name in names:
+            value = manifest[name]
+            with open(os.path.join(tmp_dir, value["filename"]), "rb") as f:
+                arr = np.frombuffer(f.read(), dtype=np.float32).copy().reshape(value["shape"])
+                arrays.append(arr)
+
+    shapes = []
+    for name, arr in zip(names, arrays):
+        shapes.append(dict(
+            name=name,
+            shape=arr.shape,
+        ))
+
+    flat = np.hstack([arr.reshape(-1) for arr in arrays])
+
+    start = time.time()
+    index = log_quantize(flat, mu=255, bins=256).astype(np.float32)
+    print("index found in %0.2fs" % (time.time() - start))
+
+    print("quantizing")
+    encoded = np.zeros(flat.shape, dtype=np.uint8)
+    elem_count = 0
+    for i, x in enumerate(flat):
+        distances = np.abs(index - x)
+        nearest = np.argmin(distances)
+        encoded[i] = nearest
+        elem_count += 1
+        if elem_count % 1000000 == 0:
+            print("rate", int(elem_count / (time.time() - start)))
+
+    with open(args.output_file, "wb") as f:
+        def write(name, buf):
+            print("%s bytes %d" % (name, len(buf)))
+            f.write(struct.pack(">L", len(buf)))
+            f.write(buf)
+
+        write("shape", json.dumps(shapes).encode("utf8"))
+        write("index", index.tobytes())
+        write("encoded", encoded.tobytes())
+
+main()
\ No newline at end of file
diff --git a/server/tools/export-example-model.py b/server/tools/export-example-model.py
deleted file mode 100644
index 92997fdda698a734d0ce72cca008fdba565bb94e..0000000000000000000000000000000000000000
--- a/server/tools/export-example-model.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import tensorflow as tf
-import json
-import os
-import argparse
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--output_dir", required=True, help="directory to put exported model in")
-a = parser.parse_args()
-
-
-def main():
-    if not os.path.exists(a.output_dir):
-        os.makedirs(a.output_dir)
-
-    input = tf.placeholder(tf.string, shape=[1])
-    key = tf.placeholder(tf.string, shape=[1])
-
-    in_data = tf.decode_base64(input[0])
-    img = tf.image.decode_png(in_data)
-    img = tf.image.rgb_to_grayscale(img)
-    out_data = tf.image.encode_png(img)
-    output = tf.convert_to_tensor([tf.encode_base64(out_data)])
-
-    variable_to_allow_model_saving = tf.Variable(1, dtype=tf.float32)
-
-    inputs = {
-        "key": key.name,
-        "input": input.name
-    }
-    tf.add_to_collection("inputs", json.dumps(inputs))
-    outputs = {
-        "key":  tf.identity(key).name,
-        "output": output.name,
-    }
-    tf.add_to_collection("outputs", json.dumps(outputs))
-
-    init_op = tf.global_variables_initializer()
-    with tf.Session() as sess:
-        sess.run(init_op)
-        saver = tf.train.Saver()
-        saver.export_meta_graph(filename=os.path.join(a.output_dir, "export.meta"))
-        saver.save(sess, os.path.join(a.output_dir, "export"), write_meta_graph=False)
-    
-    print("exported example model to %s" % a.output_dir)
-
-main()
diff --git a/server/tools/process-cloud.py b/server/tools/process-cloud.py
deleted file mode 100644
index 4f0c9396c098580335475fb3aa06ff8896d3ad9e..0000000000000000000000000000000000000000
--- a/server/tools/process-cloud.py
+++ /dev/null
@@ -1,44 +0,0 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import argparse
-import json
-import base64
-import oauth2client.service_account
-import googleapiclient.discovery
-
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--model_name", required=True, help="name of Cloud Machine Learning model")
-parser.add_argument("--input_file", required=True, help="input PNG image file")
-parser.add_argument("--output_file", required=True, help="output PNG image file")
-parser.add_argument("--credentials", required=True, help="JSON credentials for a Google Cloud Platform service account")
-a = parser.parse_args()
-
-scopes = ["https://www.googleapis.com/auth/cloud-platform"]
-credentials = oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name(a.credentials, scopes)
-ml = googleapiclient.discovery.build("ml", "v1beta1", credentials=credentials)
-
-
-def main():
-    with open(a.credentials) as f:
-        project_id = json.loads(f.read())["project_id"]
-
-    with open(a.input_file) as f:
-        input_data = f.read()
-
-    input_instance = dict(input=base64.urlsafe_b64encode(input_data), key="0")
-    input_instance = json.loads(json.dumps(input_instance))
-    request = ml.projects().predict(name="projects/" + project_id + "/models/" + a.model_name, body={"instances": [input_instance]})
-    response = request.execute()
-    output_instance = json.loads(json.dumps(response["predictions"][0]))
-
-    b64data = output_instance["output"].encode("ascii")
-    b64data += "=" * (-len(b64data) % 4)
-    output_data = base64.urlsafe_b64decode(b64data)
-
-    with open(a.output_file, "w") as f:
-        f.write(output_data)
-
-main()
\ No newline at end of file
diff --git a/server/tools/process-local.py b/server/tools/process-local.py
deleted file mode 100644
index 269096cbe0425d4da1cdca2e7c450966f6ab0d60..0000000000000000000000000000000000000000
--- a/server/tools/process-local.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import tensorflow as tf
-import numpy as np
-import argparse
-import json
-import base64
-
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--model_dir", required=True, help="directory containing exported model")
-parser.add_argument("--input_file", required=True, help="input PNG image file")
-parser.add_argument("--output_file", required=True, help="output PNG image file")
-a = parser.parse_args()
-
-def main():
-    with open(a.input_file, "rb") as f:
-        input_data = f.read()
-
-    input_instance = dict(input=base64.urlsafe_b64encode(input_data).decode("ascii"), key="0")
-    input_instance = json.loads(json.dumps(input_instance))
-
-    with tf.Session() as sess:
-        saver = tf.train.import_meta_graph(a.model_dir + "/export.meta")
-        saver.restore(sess, a.model_dir + "/export")
-        input_vars = json.loads(tf.get_collection("inputs")[0])
-        output_vars = json.loads(tf.get_collection("outputs")[0])
-        input = tf.get_default_graph().get_tensor_by_name(input_vars["input"])
-        output = tf.get_default_graph().get_tensor_by_name(output_vars["output"])
-
-        input_value = np.array(input_instance["input"])
-        output_value = sess.run(output, feed_dict={input: np.expand_dims(input_value, axis=0)})[0]
-
-    output_instance = dict(output=output_value.decode("ascii"), key="0")
-
-    b64data = output_instance["output"]
-    b64data += "=" * (-len(b64data) % 4)
-    output_data = base64.urlsafe_b64decode(b64data.encode("ascii"))
-
-    with open(a.output_file, "wb") as f:
-        f.write(output_data)
-
-main()
diff --git a/server/tools/process-remote.py b/server/tools/process-remote.py
deleted file mode 100644
index 870472f3e327c9a5a8f7c7902b88fd935c86c4a2..0000000000000000000000000000000000000000
--- a/server/tools/process-remote.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-try:
-    from urllib.request import urlopen # python 3
-except ImportError:
-    from urllib2 import urlopen # python 2
-import argparse
-
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--input_file", required=True, help="input PNG image file")
-parser.add_argument("--url", required=True, help="url to use for processing")
-parser.add_argument("--output_file", required=True, help="output PNG image file")
-a = parser.parse_args()
-
-
-def main():
-    with open(a.input_file, "rb") as f:
-        input_data = f.read()
-
-    output_data = urlopen(a.url, data=input_data).read()
-
-    with open(a.output_file, "wb") as f:
-        f.write(output_data)
-
-main()
diff --git a/server/tools/rolling-update.py b/server/tools/rolling-update.py
deleted file mode 100644
index c483cf92b6026e47f9f7d46d8c48e58d8dafd35f..0000000000000000000000000000000000000000
--- a/server/tools/rolling-update.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import subprocess
-import json
-
-
-def main():
-    output = subprocess.check_output("gcloud compute instance-groups managed list-instances pix2pix-manager --zone us-central1-c --format json", shell=True)
-    instances = json.loads(output)
-    for i, instance in enumerate(instances):
-        name = instance["instance"].split("/")[-1]
-        print("recreating %s (%d/%d)" % (name, i+1, len(instances)))
-        subprocess.check_call("gcloud compute instance-groups managed recreate-instances pix2pix-manager --zone us-central1-c --instances " + name, shell=True)
-        subprocess.check_call("gcloud compute instance-groups managed wait-until-stable pix2pix-manager --zone us-central1-c", shell=True)
-
-main()
\ No newline at end of file
diff --git a/server/tools/upload-image.py b/server/tools/upload-image.py
deleted file mode 100644
index 358465396105acc403f26123fe4b89646309dad7..0000000000000000000000000000000000000000
--- a/server/tools/upload-image.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import argparse
-import subprocess
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--version", required=True, help="version to build")
-parser.add_argument("--project", required=True, help="Google Cloud Project to use")
-a = parser.parse_args()
-
-def main():
-    version_tag = "us.gcr.io/%s/pix2pix-server:%s" % (a.project, a.version)
-    latest_tag = "us.gcr.io/%s/pix2pix-server:latest" % (a.project)
-
-    subprocess.check_call("docker build --tag %s ." % version_tag, shell=True)
-    subprocess.check_call("docker tag %s %s" % (version_tag, latest_tag), shell=True)
-    for tag in [version_tag, latest_tag]:
-        subprocess.check_call("gcloud docker -- push %s" % tag, shell=True)
-
-main()
diff --git a/server/tools/upload-model.py b/server/tools/upload-model.py
deleted file mode 100644
index ff10bcac982553e753331c29795fd782ab130be0..0000000000000000000000000000000000000000
--- a/server/tools/upload-model.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import argparse
-import json
-import os
-import time
-import sys
-import base64
-import oauth2client.service_account
-import googleapiclient.discovery
-import google.cloud.storage
-
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--bucket", required=True, help="Google Cloud Storage bucket to upload to")
-parser.add_argument("--model_name", required=True, help="name of Google Cloud Machine Learning model to create or update")
-parser.add_argument("--model_dir", required=True, help="path to directory containing exported model")
-parser.add_argument("--runtime_version", default="0.12", help="tensorflow version to use for the model")
-parser.add_argument("--credentials", help="JSON credentials for a Google Cloud Platform service account")
-parser.add_argument("--project", help="Google Cloud Project to use to override project detection")
-a = parser.parse_args()
-
-scopes = ["https://www.googleapis.com/auth/cloud-platform"]
-if a.credentials is None:
-    credentials = oauth2client.client.GoogleCredentials.get_application_default()
-    storage = google.cloud.storage.Client()
-    project_id = storage.project
-    if a.project is not None:
-        project_id = a.project
-else:
-    credentials = oauth2client.service_account.ServiceAccountCredentials.from_json_keyfile_name(a.credentials, scopes)
-    with open(a.credentials) as f:
-        project_id = json.loads(f.read())["project_id"]
-    storage = google.cloud.storage.Client.from_service_account_json(a.credentials, project=project_id)
-
-ml = googleapiclient.discovery.build("ml", "v1beta1", credentials=credentials)
-
-
-def main():
-    try:
-        bucket = storage.get_bucket(a.bucket)
-    except google.cloud.exceptions.NotFound as e:
-        print("creating bucket %s" % a.bucket)
-        bucket = storage.create_bucket(a.bucket)
-    
-    project_path = "projects/%s" % project_id
-    model_path = "%s/models/%s" % (project_path, a.model_name)
-
-    try:
-        ml.projects().models().get(name=model_path).execute()
-    except googleapiclient.errors.HttpError as e:
-        if e.resp["status"] != "404":
-            raise
-        print("creating model %s" % a.model_name)
-        ml.projects().models().create(parent=project_path, body=dict(name=a.model_name)).execute()
-
-    version_number = 0
-    resp = ml.projects().models().versions().list(parent=model_path).execute()
-    for version in resp.get("versions", []):
-        name = version["name"]
-        number = int(name.split("/")[-1][1:])
-        if number > version_number:
-            version_number = number
-    
-    version_number += 1
-    print("creating version v%d" % version_number)
-
-    for filename in os.listdir(a.model_dir):
-        if not filename.startswith("export.") and filename != "checkpoint":
-            continue
-
-        print("uploading", filename)
-        filepath = os.path.join(a.model_dir, filename)
-        blob = bucket.blob("%s-v%d/%s" % (a.model_name, version_number, filename))
-        blob.upload_from_filename(filepath)
-
-    version_path = "%s/versions/v%d" % (model_path, version_number)
-    version = dict(
-        name="v%d" % version_number,
-        runtimeVersion=a.runtime_version,
-        deploymentUri="gs://%s/%s-v%d/" % (a.bucket, a.model_name, version_number),
-    )
-    operation = ml.projects().models().versions().create(parent=model_path, body=version).execute()
-
-    sys.stdout.write("waiting for creation to finish")
-    while True:
-        operation = ml.projects().operations().get(name=operation["name"]).execute()
-        if "done" in operation and operation["done"]:
-            break
-        sys.stdout.write(".")
-        sys.stdout.flush()
-        time.sleep(10)
-    print()
-
-    print("setting version %d as default" % version_number)
-    ml.projects().models().versions().setDefault(name=version_path, body=dict()).execute()
-
-
-main()
\ No newline at end of file
diff --git a/tools/dockrun.py b/tools/dockrun.py
index b3e8d473cc36a0909ea6b769528af8de221ef329..594af1fe9e7829aa81554bce94b30f06eeadbde4 100644
--- a/tools/dockrun.py
+++ b/tools/dockrun.py
@@ -4,10 +4,7 @@ from __future__ import print_function
 
 import os
 import sys
-import argparse
-
-parser = argparse.ArgumentParser()
-parser.add_argument("--port", type=int, help="port to publish from the container")
+import shlex
 
 # from python 3.3 source
 # https://github.com/python/cpython/blob/master/Lib/shutil.py
@@ -73,21 +70,13 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None):
 
 
 def main():
-    args = sys.argv[1:]
-    i = 0
-    while i < len(args):
-        if not args[i].startswith("--"):
-            break
-        i += 2
-    
-    a = parser.parse_args(args[:i])
-    cmd = args[i:]
+    cmd = sys.argv[1:]
 
     # check if nvidia-docker or docker are on path
     docker_path = which("nvidia-docker")
     if docker_path is None:
         docker_path = which("docker")
-    
+
     if docker_path is None:
         raise Exception("docker not found")
 
@@ -103,14 +92,15 @@ def main():
         "CUDA_CACHE_PATH=/host/tmp/cuda-cache",
     ]
 
-    if a.port is not None:
-        docker_args += ["--publish", "%d:%d" % (a.port, a.port)]
+    if "CUDA_VISIBLE_DEVICES" in os.environ:
+        docker_args.extend(["--env", "CUDA_VISIBLE_DEVICES=%s" % os.environ["CUDA_VISIBLE_DEVICES"]])
 
-    args = [docker_path, "run"] + docker_args + ["affinelayer/pix2pix-tensorflow:v2"] + cmd
+    args = [docker_path, "run"] + docker_args + ["affinelayer/pix2pix-tensorflow:v3"] + cmd
 
     if not os.access("/var/run/docker.sock", os.R_OK):
         args = ["sudo"] + args
 
+    print("running", " ".join(shlex.quote(a) for a in args))
     os.execvp(args[0], args)
 
 
diff --git a/tools/test.py b/tools/test.py
index ba6fe86d8e2cf520680245f8f06a4645b70c3068..d31afa88c6f6e8755a07bb94441ff16e692ba091 100644
--- a/tools/test.py
+++ b/tools/test.py
@@ -6,61 +6,58 @@ import subprocess
 import os
 import sys
 import time
-import argparse
+import shutil
+import shlex
 
+INPUT_DIR = os.path.abspath("../data")
+OUTPUT_DIR = os.path.expanduser("~/data/pix2pix/test")
 
-parser = argparse.ArgumentParser()
-parser.add_argument("--long", action="store_true")
-a = parser.parse_args()
 
+def main():
+    start = time.time()
 
-def run(cmd, image="affinelayer/pix2pix-tensorflow"):
-    docker = "docker"
-    if sys.platform.startswith("linux"):
-        docker = "nvidia-docker"
+    images = {
+        "affinelayer": "affinelayer/pix2pix-tensorflow:v3",
+        # "py2-tensorflow": "tensorflow/tensorflow:1.4.1-gpu",
+        # "py3-tensorflow": "tensorflow/tensorflow:1.4.1-gpu-py3",
+    }
 
-    datapath = os.path.abspath("../data")
-    prefix = [docker, "run", "--rm", "--volume", os.getcwd() + ":/prj", "--volume", datapath + ":/data", "--workdir", "/prj", "--env", "PYTHONUNBUFFERED=x", "--volume", "/tmp/cuda-cache:/cuda-cache", "--env", "CUDA_CACHE_PATH=/cuda-cache", image]
-    args = prefix + cmd.split(" ")
-    print(" ".join(args))
-    subprocess.check_call(args)
+    if os.path.exists(OUTPUT_DIR):
+        shutil.rmtree(OUTPUT_DIR)
 
+    for image_name, image in images.items():
+        def run(cmd):
+            docker = "docker"
+            if sys.platform.startswith("linux"):
+                docker = "nvidia-docker"
 
-def main():
-    start = time.time()
+            prefix = [docker, "run", "--rm", "--volume", os.getcwd() + ":/prj", "--volume", INPUT_DIR + ":/input", "--volume", os.path.join(OUTPUT_DIR, image_name) + ":/output","--workdir", "/prj", "--env", "PYTHONUNBUFFERED=x", "--volume", "/tmp/cuda-cache:/cuda-cache", "--env", "CUDA_CACHE_PATH=/cuda-cache", image]
+            args = prefix + shlex.split(cmd)
+            print(" ".join(args))
+            subprocess.check_call(args)
 
-    if a.long:
-        run("python pix2pix.py --mode train --output_dir test/facades_BtoA_train --max_epochs 200 --input_dir /data/official/facades/train --which_direction BtoA --seed 0")
-        run("python pix2pix.py --mode test --output_dir test/facades_BtoA_test --input_dir /data/official/facades/val --seed 0 --checkpoint test/facades_BtoA_train")
+        run("python tools/process.py --input_dir /input/pusheen/original --operation resize --output_dir /output/process_resize")
+        if image_name == "affinelayer":
+            run("python tools/process.py --input_dir /output/process_resize --operation edges --output_dir /output/process_edges")
 
-        run("python pix2pix.py --mode train --output_dir test/color-lab_AtoB_train --max_epochs 10 --input_dir /data/color-lab/train --which_direction AtoB --seed 0 --lab_colorization")
-        run("python pix2pix.py --mode test --output_dir test/color-lab_AtoB_test --input_dir /data/color-lab/val --seed 0 --checkpoint test/color-lab_AtoB_train")
-    else:
-        # training
         for direction in ["AtoB", "BtoA"]:
-            for dataset in ["facades"]:
+            for dataset in ["facades", "maps"]:
                 name = dataset + "_" + direction
-                run("python pix2pix.py --mode train --output_dir test/%s_train --max_steps 1 --input_dir /data/official/%s/train --which_direction %s --seed 0" % (name, dataset, direction))
-                run("python pix2pix.py --mode test --output_dir test/%s_test --max_steps 1 --input_dir /data/official/%s/val --seed 0 --checkpoint test/%s_train" % (name, dataset, name))
+                run("python pix2pix.py --mode train --input_dir /input/official/%s/train --output_dir /output/%s_train --display_freq 1 --max_steps 1 --which_direction %s --seed 0" % (dataset, name, direction))
+                run("python pix2pix.py --mode test --input_dir /input/official/%s/val --output_dir /output/%s_test --display_freq 1 --max_steps 1 --checkpoint /output/%s_train --seed 0" % (dataset, name, name))
 
-            # test lab colorization
             dataset = "color-lab"
             name = dataset + "_" + direction
-            run("python pix2pix.py --mode train --output_dir test/%s_train --max_steps 1 --input_dir /data/%s/train --which_direction %s --seed 0 --lab_colorization" % (name, dataset, direction))
-            run("python pix2pix.py --mode test --output_dir test/%s_test --max_steps 1 --input_dir /data/%s/val --seed 0 --checkpoint test/%s_train" % (name, dataset, name))
+            run("python pix2pix.py --mode train --input_dir /input/%s/train --output_dir /output/%s_train --display_freq 1 --max_steps 1 --which_direction %s --lab_colorization --seed 0" % (dataset, name, direction))
+            run("python pix2pix.py --mode test --input_dir /input/%s/val --output_dir /output/%s_test --display_freq 1 --max_steps 1 --checkpoint /output/%s_train --seed 0" % (dataset, name, name))
 
-        # using pretrained model (can't use pretrained models from tensorflow 0.12, so disabled for now)
+        # using pretrained model
         # for dataset, direction in [("facades", "BtoA")]:
         #     name = dataset + "_" + direction
-        #     run("python pix2pix.py --mode test --output_dir test/%s_pretrained_test --input_dir /data/official/%s/val --max_steps 100 --which_direction %s --seed 0 --checkpoint /data/pretrained/%s" % (name, dataset, direction, name))
-        #     run("python pix2pix.py --mode export --output_dir test/%s_pretrained_export --checkpoint /data/pretrained/%s" % (name, name))
-
-        # test python3
-        run("python pix2pix.py --mode train --output_dir test/py3_facades_AtoB_train --max_steps 1 --input_dir /data/official/facades/train --which_direction AtoB --seed 0", image="tensorflow/tensorflow:1.0.0-gpu-py3")
-        run("python pix2pix.py --mode test --output_dir test/py3_facades_AtoB_test --max_steps 1 --input_dir /data/official/facades/val --seed 0 --checkpoint test/py3_facades_AtoB_train", image="tensorflow/tensorflow:1.0.0-gpu-py3")
+        #     run("python pix2pix.py --mode test --output_dir test/%s_pretrained_test --input_dir /input/official/%s/val --max_steps 100 --which_direction %s --seed 0 --checkpoint /input/pretrained/%s" % (name, dataset, direction, name))
+        #     run("python pix2pix.py --mode export --output_dir test/%s_pretrained_export --checkpoint /input/pretrained/%s" % (name, name))
 
     print("elapsed", int(time.time() - start))
-    # long: about 9 hours (linux)
 
 
 main()