diff --git a/.dockerignore b/.dockerignore
index 6a5d174d3fa806a3216e35a4e9087a4458c1e192..bbd458dcf488e8c522fbae7923909f96a4327383 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,6 @@
 .coverage
 .DS_Store
+.git
 node_modules
 *.pyc
 coverage.xml
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index ead4351e3696c8fcfc89ca47cdab2509c05fa565..9bfe76f05b8f51f83587d5f11cd636e5c519e900 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ node_modules
 *.pyc
 coverage.xml
 .ipynb_checkpoints*
+.vscode
+
diff --git a/.travis.yml b/.travis.yml
index b5c30814cde493b0e4b87ccff3704b4ae6139805..75888c7cc7fa7471b1d81ec0e1c558b092c39d25 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,3 @@
-# sudo: false indicates travis should use container infrastructure
-sudo: false
 language: python
 python:
   - 2.7
@@ -8,9 +6,9 @@ python:
 before_install:
   ## courtesy of http://conda.pydata.org/docs/travis.html
   - if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then
-      wget https://repo.continuum.io/miniconda/Miniconda-latest-Linux-x86_64.sh -O miniconda.sh;
+    wget https://repo.anaconda.com/miniconda/Miniconda2-latest-Linux-x86_64.sh -O miniconda.sh;
     else
-      wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
+    wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh;
     fi
   - bash miniconda.sh -b -p $HOME/miniconda
   - export PATH="$HOME/miniconda/bin:$PATH"
@@ -20,7 +18,7 @@ before_install:
   - conda info -a
 install:
   - conda config --add channels conda-forge
-  - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION obspy pycurl nose flake8 coverage webtest 'icu=58.*'
+  - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION obspy pycurl nose flake8 coverage webtest
   - source activate test-environment
 script:
   - flake8 --config=.flake8 bin/ geomagio/ test/
diff --git a/Dockerfile b/Dockerfile
index 2a09b4a0ccb695d311b0c6e736a650de49ae860b..570ebfc8786b4ebbf0bc45b66a3858f60a4de8f8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,33 +1,36 @@
-FROM usgs/centos:latest AS pycurl-build
+ARG BUILD_IMAGE=usgs/centos:7
+ARG FROM_IMAGE=usgs/centos:7
+FROM $BUILD_IMAGE AS pycurl-build
 
 # install conda dependencies
 RUN yum install -y \
         bzip2 \
         gcc \
         libcurl-devel \
-        && \
-    yum clean all
+    && yum clean all
 
 # install conda
 ENV PATH /conda/bin:$PATH
-RUN echo 'export PATH=/conda/bin:$PATH' > /etc/profile.d/conda.sh && \
-    curl https://repo.continuum.io/miniconda/Miniconda2-latest-Linux-x86_64.sh \
-        -o ~/miniconda.sh && \
-    /bin/bash ~/miniconda.sh -b -p /conda && \
-    rm ~/miniconda.sh
-
-# install algorithms and dependencies via conda
-RUN conda config --set ssl_verify $SSL_CERT_FILE && \
-    conda config --add channels conda-forge && \
-    conda install --yes jupyter obspy 'icu=58.*' && \
-    conda clean -i -l -t -y && \
+RUN echo 'export PATH=/conda/bin:$PATH' > /etc/profile.d/conda.sh \
+    && curl \
+        https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh \
+        -o ~/miniconda.sh \
+    && /bin/bash ~/miniconda.sh -b -p /conda \
+    && rm ~/miniconda.sh
+
+# install dependencies via conda
+RUN conda config --set ssl_verify $SSL_CERT_FILE \
+    && conda config --add channels conda-forge \
+    && conda install --yes jupyter obspy \
     # build pycurl with SFTP support
-        export PIP_CERT=$SSL_CERT_FILE && \
-        export PYCURL_SSL_LIBRARY=nss && \
-        pip install pycurl
+    && export PIP_CERT=$SSL_CERT_FILE \
+    && export PYCURL_SSL_LIBRARY=nss \
+    && pip install pycurl \
+    # clean up
+    && conda clean --all -y
 
 
-FROM usgs/centos:latest
+FROM $FROM_IMAGE
 LABEL maintainer="Jeremy Fee <jmfee@usgs.gov>"
 
 # use conda install from build container
@@ -38,19 +41,19 @@ COPY --from=pycurl-build /etc/profile.d/conda.sh /etc/profile.d/conda.sh
 # copy library (ignores set in .dockerignore)
 COPY . /geomag-algorithms
 
-RUN pip install /geomag-algorithms && \
-    useradd \
+
+RUN pip install /geomag-algorithms \
+    && useradd \
         -c 'Docker image user' \
         -m \
         -r \
         -s /sbin/nologin \
-         geomag_user && \
-    mkdir -p /home/geomag_user/notebooks && \
-    chown -R geomag_user:geomag_user /home/geomag_user
+         geomag_user \
+    && mkdir -p /home/geomag_user/notebooks \
+    && chown -R geomag_user:geomag_user /home/geomag_user
 
 USER geomag_user
-
-WORKDIR /home/geomag_user
-EXPOSE 80
+WORKDIR /geomag-algorithms
+EXPOSE 8000
 # entrypoint needs double quotes
 ENTRYPOINT [ "/geomag-algorithms/docker-entrypoint.sh" ]
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000000000000000000000000000000000000..aa6d6e40021e783882d1d3418f669a40a041f4e0
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,17 @@
+[[source]]
+name = "pypi"
+url = "https://pypi.org/simple"
+verify_ssl = true
+
+[dev-packages]
+coverage = "*"
+flake8 = "*"
+nose = "*"
+webtest = "*"
+
+[packages]
+numpy = "*"
+scipy = "*"
+obspy = "*"
+pycurl = "*"
+
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000000000000000000000000000000000000..7128cb0b10d7ead63f316c0db079006732c9a3f9
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,396 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "597f6aee7828ec46583efa178894001691088a43af8b48a10c94509dd52d35ff"
+        },
+        "pipfile-spec": 6,
+        "requires": {},
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "certifi": {
+            "hashes": [
+                "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
+                "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
+            ],
+            "version": "==2019.11.28"
+        },
+        "chardet": {
+            "hashes": [
+                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
+                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+            ],
+            "version": "==3.0.4"
+        },
+        "cycler": {
+            "hashes": [
+                "sha256:1d8a5ae1ff6c5cf9b93e8811e581232ad8920aeec647c37316ceac982b08cb2d",
+                "sha256:cd7b2d1018258d7247a71425e9f26463dfb444d411c39569972f4ce586b0c9d8"
+            ],
+            "version": "==0.10.0"
+        },
+        "decorator": {
+            "hashes": [
+                "sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce",
+                "sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"
+            ],
+            "version": "==4.4.1"
+        },
+        "future": {
+            "hashes": [
+                "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
+            ],
+            "version": "==0.18.2"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
+                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+            ],
+            "version": "==2.8"
+        },
+        "kiwisolver": {
+            "hashes": [
+                "sha256:05b5b061e09f60f56244adc885c4a7867da25ca387376b02c1efc29cc16bcd0f",
+                "sha256:210d8c39d01758d76c2b9a693567e1657ec661229bc32eac30761fa79b2474b0",
+                "sha256:26f4fbd6f5e1dabff70a9ba0d2c4bd30761086454aa30dddc5b52764ee4852b7",
+                "sha256:3b15d56a9cd40c52d7ab763ff0bc700edbb4e1a298dc43715ecccd605002cf11",
+                "sha256:3b2378ad387f49cbb328205bda569b9f87288d6bc1bf4cd683c34523a2341efe",
+                "sha256:400599c0fe58d21522cae0e8b22318e09d9729451b17ee61ba8e1e7c0346565c",
+                "sha256:47b8cb81a7d18dbaf4fed6a61c3cecdb5adec7b4ac292bddb0d016d57e8507d5",
+                "sha256:53eaed412477c836e1b9522c19858a8557d6e595077830146182225613b11a75",
+                "sha256:58e626e1f7dfbb620d08d457325a4cdac65d1809680009f46bf41eaf74ad0187",
+                "sha256:5a52e1b006bfa5be04fe4debbcdd2688432a9af4b207a3f429c74ad625022641",
+                "sha256:5c7ca4e449ac9f99b3b9d4693debb1d6d237d1542dd6a56b3305fe8a9620f883",
+                "sha256:682e54f0ce8f45981878756d7203fd01e188cc6c8b2c5e2cf03675390b4534d5",
+                "sha256:76275ee077772c8dde04fb6c5bc24b91af1bb3e7f4816fd1852f1495a64dad93",
+                "sha256:79bfb2f0bd7cbf9ea256612c9523367e5ec51d7cd616ae20ca2c90f575d839a2",
+                "sha256:7f4dd50874177d2bb060d74769210f3bce1af87a8c7cf5b37d032ebf94f0aca3",
+                "sha256:8944a16020c07b682df861207b7e0efcd2f46c7488619cb55f65882279119389",
+                "sha256:8aa7009437640beb2768bfd06da049bad0df85f47ff18426261acecd1cf00897",
+                "sha256:9105ce82dcc32c73eb53a04c869b6a4bc756b43e4385f76ea7943e827f529e4d",
+                "sha256:933df612c453928f1c6faa9236161a1d999a26cd40abf1dc5d7ebbc6dbfb8fca",
+                "sha256:939f36f21a8c571686eb491acfffa9c7f1ac345087281b412d63ea39ca14ec4a",
+                "sha256:9491578147849b93e70d7c1d23cb1229458f71fc79c51d52dce0809b2ca44eea",
+                "sha256:9733b7f64bd9f807832d673355f79703f81f0b3e52bfce420fc00d8cb28c6a6c",
+                "sha256:a02f6c3e229d0b7220bd74600e9351e18bc0c361b05f29adae0d10599ae0e326",
+                "sha256:a0c0a9f06872330d0dd31b45607197caab3c22777600e88031bfe66799e70bb0",
+                "sha256:aa716b9122307c50686356cfb47bfbc66541868078d0c801341df31dca1232a9",
+                "sha256:acc4df99308111585121db217681f1ce0eecb48d3a828a2f9bbf9773f4937e9e",
+                "sha256:b64916959e4ae0ac78af7c3e8cef4becee0c0e9694ad477b4c6b3a536de6a544",
+                "sha256:d22702cadb86b6fcba0e6b907d9f84a312db9cd6934ee728144ce3018e715ee1",
+                "sha256:d3fcf0819dc3fea58be1fd1ca390851bdb719a549850e708ed858503ff25d995",
+                "sha256:d52e3b1868a4e8fd18b5cb15055c76820df514e26aa84cc02f593d99fef6707f",
+                "sha256:db1a5d3cc4ae943d674718d6c47d2d82488ddd94b93b9e12d24aabdbfe48caee",
+                "sha256:e3a21a720791712ed721c7b95d433e036134de6f18c77dbe96119eaf7aa08004",
+                "sha256:e8bf074363ce2babeb4764d94f8e65efd22e6a7c74860a4f05a6947afc020ff2",
+                "sha256:f16814a4a96dc04bf1da7d53ee8d5b1d6decfc1a92a63349bb15d37b6a263dd9",
+                "sha256:f2b22153870ca5cf2ab9c940d7bc38e8e9089fa0f7e5856ea195e1cf4ff43d5a",
+                "sha256:f790f8b3dff3d53453de6a7b7ddd173d2e020fb160baff578d578065b108a05f",
+                "sha256:fe51b79da0062f8e9d49ed0182a626a7dc7a0cbca0328f612c6ee5e4711c81e4"
+            ],
+            "version": "==1.1.0"
+        },
+        "lxml": {
+            "hashes": [
+                "sha256:00ac0d64949fef6b3693813fe636a2d56d97a5a49b5bbb86e4cc4cc50ebc9ea2",
+                "sha256:0571e607558665ed42e450d7bf0e2941d542c18e117b1ebbf0ba72f287ad841c",
+                "sha256:0e3f04a7615fdac0be5e18b2406529521d6dbdb0167d2a690ee328bef7807487",
+                "sha256:13cf89be53348d1c17b453867da68704802966c433b2bb4fa1f970daadd2ef70",
+                "sha256:217262fcf6a4c2e1c7cb1efa08bd9ebc432502abc6c255c4abab611e8be0d14d",
+                "sha256:223e544828f1955daaf4cefbb4853bc416b2ec3fd56d4f4204a8b17007c21250",
+                "sha256:277cb61fede2f95b9c61912fefb3d43fbd5f18bf18a14fae4911b67984486f5d",
+                "sha256:3213f753e8ae86c396e0e066866e64c6b04618e85c723b32ecb0909885211f74",
+                "sha256:4690984a4dee1033da0af6df0b7a6bde83f74e1c0c870623797cec77964de34d",
+                "sha256:4fcc472ef87f45c429d3b923b925704aa581f875d65bac80f8ab0c3296a63f78",
+                "sha256:61409bd745a265a742f2693e4600e4dbd45cc1daebe1d5fad6fcb22912d44145",
+                "sha256:678f1963f755c5d9f5f6968dded7b245dd1ece8cf53c1aa9d80e6734a8c7f41d",
+                "sha256:6c6d03549d4e2734133badb9ab1c05d9f0ef4bcd31d83e5d2b4747c85cfa21da",
+                "sha256:6e74d5f4d6ecd6942375c52ffcd35f4318a61a02328f6f1bd79fcb4ffedf969e",
+                "sha256:7b4fc7b1ecc987ca7aaf3f4f0e71bbfbd81aaabf87002558f5bc95da3a865bcd",
+                "sha256:7ed386a40e172ddf44c061ad74881d8622f791d9af0b6f5be20023029129bc85",
+                "sha256:8f54f0924d12c47a382c600c880770b5ebfc96c9fd94cf6f6bdc21caf6163ea7",
+                "sha256:ad9b81351fdc236bda538efa6879315448411a81186c836d4b80d6ca8217cdb9",
+                "sha256:bbd00e21ea17f7bcc58dccd13869d68441b32899e89cf6cfa90d624a9198ce85",
+                "sha256:c3c289762cc09735e2a8f8a49571d0e8b4f57ea831ea11558247b5bdea0ac4db",
+                "sha256:cf4650942de5e5685ad308e22bcafbccfe37c54aa7c0e30cd620c2ee5c93d336",
+                "sha256:cfcbc33c9c59c93776aa41ab02e55c288a042211708b72fdb518221cc803abc8",
+                "sha256:e301055deadfedbd80cf94f2f65ff23126b232b0d1fea28f332ce58137bcdb18",
+                "sha256:ebbfe24df7f7b5c6c7620702496b6419f6a9aa2fd7f005eb731cc80d7b4692b9",
+                "sha256:eff69ddbf3ad86375c344339371168640951c302450c5d3e9936e98d6459db06",
+                "sha256:f6ed60a62c5f1c44e789d2cf14009423cb1646b44a43e40a9cf6a21f077678a1"
+            ],
+            "version": "==4.4.2"
+        },
+        "matplotlib": {
+            "hashes": [
+                "sha256:08ccc8922eb4792b91c652d3e6d46b1c99073f1284d1b6705155643e8046463a",
+                "sha256:161dcd807c0c3232f4dcd4a12a382d52004a498174cbfafd40646106c5bcdcc8",
+                "sha256:1f9e885bfa1b148d16f82a6672d043ecf11197f6c71ae222d0546db706e52eb2",
+                "sha256:2d6ab54015a7c0d727c33e36f85f5c5e4172059efdd067f7527f6e5d16ad01aa",
+                "sha256:5d2e408a2813abf664bd79431107543ecb449136912eb55bb312317edecf597e",
+                "sha256:61c8b740a008218eb604de518eb411c4953db0cb725dd0b32adf8a81771cab9e",
+                "sha256:80f10af8378fccc136da40ea6aa4a920767476cdfb3241acb93ef4f0465dbf57",
+                "sha256:819d4860315468b482f38f1afe45a5437f60f03eaede495d5ff89f2eeac89500",
+                "sha256:8cc0e44905c2c8fda5637cad6f311eb9517017515a034247ab93d0cf99f8bb7a",
+                "sha256:8e8e2c2fe3d873108735c6ee9884e6f36f467df4a143136209cff303b183bada",
+                "sha256:98c2ffeab8b79a4e3a0af5dd9939f92980eb6e3fec10f7f313df5f35a84dacab",
+                "sha256:d59bb0e82002ac49f4152963f8a1079e66794a4f454457fd2f0dcc7bf0797d30",
+                "sha256:ee59b7bb9eb75932fe3787e54e61c99b628155b0cedc907864f24723ba55b309"
+            ],
+            "version": "==3.1.2"
+        },
+        "numpy": {
+            "hashes": [
+                "sha256:0a7a1dd123aecc9f0076934288ceed7fd9a81ba3919f11a855a7887cbe82a02f",
+                "sha256:0c0763787133dfeec19904c22c7e358b231c87ba3206b211652f8cbe1241deb6",
+                "sha256:3d52298d0be333583739f1aec9026f3b09fdfe3ddf7c7028cb16d9d2af1cca7e",
+                "sha256:43bb4b70585f1c2d153e45323a886839f98af8bfa810f7014b20be714c37c447",
+                "sha256:475963c5b9e116c38ad7347e154e5651d05a2286d86455671f5b1eebba5feb76",
+                "sha256:64874913367f18eb3013b16123c9fed113962e75d809fca5b78ebfbb73ed93ba",
+                "sha256:683828e50c339fc9e68720396f2de14253992c495fdddef77a1e17de55f1decc",
+                "sha256:6ca4000c4a6f95a78c33c7dadbb9495c10880be9c89316aa536eac359ab820ae",
+                "sha256:75fd817b7061f6378e4659dd792c84c0b60533e867f83e0d1e52d5d8e53df88c",
+                "sha256:7d81d784bdbed30137aca242ab307f3e65c8d93f4c7b7d8f322110b2e90177f9",
+                "sha256:8d0af8d3664f142414fd5b15cabfd3b6cc3ef242a3c7a7493257025be5a6955f",
+                "sha256:9679831005fb16c6df3dd35d17aa31dc0d4d7573d84f0b44cc481490a65c7725",
+                "sha256:a8f67ebfae9f575d85fa859b54d3bdecaeece74e3274b0b5c5f804d7ca789fe1",
+                "sha256:acbf5c52db4adb366c064d0b7c7899e3e778d89db585feadd23b06b587d64761",
+                "sha256:ada4805ed51f5bcaa3a06d3dd94939351869c095e30a2b54264f5a5004b52170",
+                "sha256:c7354e8f0eca5c110b7e978034cd86ed98a7a5ffcf69ca97535445a595e07b8e",
+                "sha256:e2e9d8c87120ba2c591f60e32736b82b67f72c37ba88a4c23c81b5b8fa49c018",
+                "sha256:e467c57121fe1b78a8f68dd9255fbb3bb3f4f7547c6b9e109f31d14569f490c3",
+                "sha256:ede47b98de79565fcd7f2decb475e2dcc85ee4097743e551fe26cfc7eb3ff143",
+                "sha256:f58913e9227400f1395c7b800503ebfdb0772f1c33ff8cb4d6451c06cabdf316",
+                "sha256:fe39f5fd4103ec4ca3cb8600b19216cd1ff316b4990f4c0b6057ad982c0a34d5"
+            ],
+            "index": "pypi",
+            "version": "==1.17.4"
+        },
+        "obspy": {
+            "hashes": [
+                "sha256:14e7ff1b87c2cb901f3a6bebb64cbc5e8d624a067319c242b55cf77c341018fc",
+                "sha256:17786240e76629b2e2004232fe97a988c9de5f91e2d3367906598760e0f86fa8",
+                "sha256:20dbfdfffcbcea675f3749132eddf7ec4fb4e1defb23372573dcb8669651aa9d",
+                "sha256:290dea98f7a8577911dae7816aae9117adc536b0baf6dde1db0b02c53b108a96",
+                "sha256:5693b17ef7e7b62e69d28af0732d8f53beea35f64aba3c6a4e0b0996e1f5123e",
+                "sha256:6141112cf264c7baf345333f6b32effefdc268098f499cc1bc7f14587af17149",
+                "sha256:6268fceec0a8e717fb3eafd9cf25cc2ead9f956e3e7149ca10124ee488a9901c",
+                "sha256:ce6fe921980bd877d91abda2b1046b95222b304ba2f50800a932979c87ec1c82",
+                "sha256:d4f61569702a9cbc4bd00ea54589437ee220edd68c56168c6ebf6710e7e93bcd"
+            ],
+            "index": "pypi",
+            "version": "==1.1.1"
+        },
+        "pycurl": {
+            "hashes": [
+                "sha256:6f08330c5cf79fa8ef68b9912b9901db7ffd34b63e225dce74db56bb21deda8e"
+            ],
+            "index": "pypi",
+            "version": "==7.43.0.3"
+        },
+        "pyparsing": {
+            "hashes": [
+                "sha256:20f995ecd72f2a1f4bf6b072b63b22e2eb457836601e76d6e5dfcd75436acc1f",
+                "sha256:4ca62001be367f01bd3e92ecbb79070272a9d4964dce6a48a82ff0b8bc7e683a"
+            ],
+            "version": "==2.4.5"
+        },
+        "python-dateutil": {
+            "hashes": [
+                "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c",
+                "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"
+            ],
+            "version": "==2.8.1"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
+                "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
+            ],
+            "version": "==2.22.0"
+        },
+        "scipy": {
+            "hashes": [
+                "sha256:0b8c9dc042b9a47912b18b036b4844029384a5b8d89b64a4901ac3e06876e5f6",
+                "sha256:18ad034be955df046b5a27924cdb3db0e8e1d76aaa22c635403fe7aee17f1482",
+                "sha256:225d0b5e140bb66df23d438c7b535303ce8e533f94454f4e5bde5f8d109103ea",
+                "sha256:2f690ba68ed7caa7c30b6dc48c1deed22c78f3840fa4736083ef4f2bd8baa19e",
+                "sha256:4b8746f4a755bdb2eeb39d6e253a60481e165cfd74fdfb54d27394bd2c9ec8ac",
+                "sha256:4ba2ce1a58fe117e993cf316a149cf9926c7c5000c0cdc4bc7c56ae8325612f6",
+                "sha256:546f0dc020b155b8711159d53c87b36591d31f3327c47974a4fb6b50d91589c2",
+                "sha256:583f2ccd6a112656c9feb2345761d2b19e9213a094cfced4e7d2c1cae4173272",
+                "sha256:64bf4e8ae0db2d42b58477817f648d81e77f0b381d0ea4427385bba3f959380a",
+                "sha256:7be424ee09bed7ced36c9457f99c826ce199fd0c0f5b272cf3d098ff7b29e3ae",
+                "sha256:869465c7ff89fc0a1e2ea1642b0c65f1b3c05030f3a4c0d53d6a57b2dba7c242",
+                "sha256:884e619821f47eccd42979488d10fa1e15dbe9f3b7660b1c8c928d203bd3c1a3",
+                "sha256:a42b0d02150ef4747e225c31c976a304de5dc8202ec35a27111b7bb8176e5f13",
+                "sha256:a70308bb065562afb936c963780deab359966d71ab4f230368b154dde3136ea4",
+                "sha256:b01ea5e4cf95a93dc335089f8fbe97852f56fdb74afff238cbdf09793103b6b7",
+                "sha256:b7b8cf45f9a48f23084f19deb9384a1cccb5e92fbc879b12f97dc4d56fb2eb92",
+                "sha256:bb0899d3f8b9fe8ef95b79210cf0deb6709542889fadaa438eeb3a28001e09e7",
+                "sha256:c008f1b58f99f1d1cc546957b3effe448365e0a217df1f1894e358906e91edad",
+                "sha256:cfee99d085d562a7e3c4afe51ac1fe9b434363489e565a130459307f30077973",
+                "sha256:dfcb0f0a2d8e958611e0b56536285bb435f03746b6feac0e29f045f7c6caf164",
+                "sha256:f5d47351aeb1cb6bda14a8908e56648926a6b2d714f89717c71f7ada41282141"
+            ],
+            "index": "pypi",
+            "version": "==1.3.3"
+        },
+        "six": {
+            "hashes": [
+                "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
+                "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
+            ],
+            "version": "==1.13.0"
+        },
+        "sqlalchemy": {
+            "hashes": [
+                "sha256:afa5541e9dea8ad0014251bc9d56171ca3d8b130c9627c6cb3681cff30be3f8a"
+            ],
+            "version": "==1.3.11"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:a8a318824cc77d1fd4b2bec2ded92646630d7fe8619497b142c84a9e6f5a7293",
+                "sha256:f3c5fd51747d450d4dcf6f923c81f78f811aab8205fda64b0aba34a4e48b0745"
+            ],
+            "version": "==1.25.7"
+        }
+    },
+    "develop": {
+        "beautifulsoup4": {
+            "hashes": [
+                "sha256:5279c36b4b2ec2cb4298d723791467e3000e5384a43ea0cdf5d45207c7e97169",
+                "sha256:6135db2ba678168c07950f9a16c4031822c6f4aec75a65e0a97bc5ca09789931",
+                "sha256:dcdef580e18a76d54002088602eba453eec38ebbcafafeaabd8cab12b6155d57"
+            ],
+            "version": "==4.8.1"
+        },
+        "coverage": {
+            "hashes": [
+                "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6",
+                "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650",
+                "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5",
+                "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d",
+                "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351",
+                "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755",
+                "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef",
+                "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca",
+                "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca",
+                "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9",
+                "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc",
+                "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5",
+                "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f",
+                "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe",
+                "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888",
+                "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5",
+                "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce",
+                "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5",
+                "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e",
+                "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e",
+                "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9",
+                "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437",
+                "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1",
+                "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c",
+                "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24",
+                "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47",
+                "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2",
+                "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28",
+                "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c",
+                "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7",
+                "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0",
+                "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025"
+            ],
+            "index": "pypi",
+            "version": "==4.5.4"
+        },
+        "entrypoints": {
+            "hashes": [
+                "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19",
+                "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"
+            ],
+            "version": "==0.3"
+        },
+        "flake8": {
+            "hashes": [
+                "sha256:45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb",
+                "sha256:49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"
+            ],
+            "index": "pypi",
+            "version": "==3.7.9"
+        },
+        "mccabe": {
+            "hashes": [
+                "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
+                "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+            ],
+            "version": "==0.6.1"
+        },
+        "nose": {
+            "hashes": [
+                "sha256:9ff7c6cc443f8c51994b34a667bbcf45afd6d945be7477b52e97516fd17c53ac",
+                "sha256:dadcddc0aefbf99eea214e0f1232b94f2fa9bd98fa8353711dacb112bfcbbb2a",
+                "sha256:f1bffef9cbc82628f6e7d7b40d7e255aefaa1adb6a1b1d26c69a8b79e6208a98"
+            ],
+            "index": "pypi",
+            "version": "==1.3.7"
+        },
+        "pycodestyle": {
+            "hashes": [
+                "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56",
+                "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"
+            ],
+            "version": "==2.5.0"
+        },
+        "pyflakes": {
+            "hashes": [
+                "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0",
+                "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"
+            ],
+            "version": "==2.1.1"
+        },
+        "six": {
+            "hashes": [
+                "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd",
+                "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"
+            ],
+            "version": "==1.13.0"
+        },
+        "soupsieve": {
+            "hashes": [
+                "sha256:bdb0d917b03a1369ce964056fc195cfdff8819c40de04695a80bc813c3cfa1f5",
+                "sha256:e2c1c5dee4a1c36bcb790e0fabd5492d874b8ebd4617622c4f6a731701060dda"
+            ],
+            "version": "==1.9.5"
+        },
+        "waitress": {
+            "hashes": [
+                "sha256:278e09d6849acc1365404bbf7d790d0423b159802e850c726e8cd0a126a2dac7",
+                "sha256:f103e557725b17ae3c62f9e6005cdd85103f8b68fa86cf7c764ba7adc38ca5a2"
+            ],
+            "version": "==1.3.1"
+        },
+        "webob": {
+            "hashes": [
+                "sha256:05aaab7975e0ee8af2026325d656e5ce14a71f1883c52276181821d6d5bf7086",
+                "sha256:36db8203c67023d68c1b00208a7bf55e3b10de2aa317555740add29c619de12b"
+            ],
+            "version": "==1.8.5"
+        },
+        "webtest": {
+            "hashes": [
+                "sha256:41348efe4323a647a239c31cde84e5e440d726ca4f449859264e538d39037fd0",
+                "sha256:f3a603b8f1dd873b9710cd5a7dd0889cf758d7e1c133b1dae971c04f567e566e"
+            ],
+            "index": "pypi",
+            "version": "==2.0.33"
+        }
+    }
+}
diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh
index 14f4954d147b505599e20b383b51e81740e49cd2..40d4d80dcc7dcd9569df58959fe01a3709b3b32d 100755
--- a/docker-entrypoint.sh
+++ b/docker-entrypoint.sh
@@ -10,6 +10,8 @@ _term () {
 }
 trap _term SIGTERM
 
+# add geomagio to notebook path
+export PYTHONPATH=/geomag-algorithms
 
 # run jupyter notebook server
 jupyter notebook \
diff --git a/geomagio/Controller.py b/geomagio/Controller.py
index 636377d8dc8cd9b362ef1397cc740a13b3cb8c2c..fc3cf19c48bba3262a83da39f94cb75eca3d831c 100644
--- a/geomagio/Controller.py
+++ b/geomagio/Controller.py
@@ -307,7 +307,7 @@ def get_input_factory(args):
 
     # standard arguments
     input_factory_args = {}
-    input_factory_args['interval'] = args.interval
+    input_factory_args['interval'] = args.input_interval or args.interval
     input_factory_args['observatory'] = args.observatory
     input_factory_args['type'] = args.type
     # stream/url arguments
@@ -328,6 +328,12 @@ def get_input_factory(args):
                 port=args.input_port,
                 locationCode=args.locationcode,
                 **input_factory_args)
+    elif input_type == 'miniseed':
+        input_factory = edge.MiniSeedFactory(
+                host=args.input_host,
+                port=args.input_port,
+                locationCode=args.locationcode,
+                **input_factory_args)
     elif input_type == 'goes':
         # TODO: deal with other goes arguments
         input_factory = imfv283.GOESIMFV283Factory(
@@ -375,7 +381,7 @@ def get_output_factory(args):
 
     # standard arguments
     output_factory_args = {}
-    output_factory_args['interval'] = args.interval
+    output_factory_args['interval'] = args.output_interval or args.interval
     output_factory_args['observatory'] = args.output_observatory
     output_factory_args['type'] = args.type
     # stream/url arguments
@@ -658,7 +664,13 @@ def parse_args(args):
             type=edge.LocationCode)
     parser.add_argument('--interval',
             default='minute',
-            choices=['hourly', 'minute', 'second'])
+            choices=['day', 'hour', 'minute', 'second', 'tenhertz'])
+    parser.add_argument('--input-interval',
+            default=None,
+            choices=['day', 'hour', 'minute', 'second', 'tenhertz'])
+    parser.add_argument('--output-interval',
+            default=None,
+            choices=['day', 'hour', 'minute', 'second', 'tenhertz'])
     parser.add_argument('--update',
             action='store_true',
             default=False,
@@ -740,6 +752,7 @@ def parse_args(args):
                 'iaga2002',
                 'imfv122',
                 'imfv283',
+                'miniseed',
                 'pcdcp'))
 
     parser.add_argument('--input-file',
diff --git a/geomagio/TimeseriesUtility.py b/geomagio/TimeseriesUtility.py
index 7a316ae2432972d3794189b3519f4f4226bbb2b8..046c10d3fc58f17c8cd5ebb351a7adcf4d6bff07 100644
--- a/geomagio/TimeseriesUtility.py
+++ b/geomagio/TimeseriesUtility.py
@@ -35,13 +35,15 @@ def create_empty_trace(starttime, endtime, observatory,
     obspy.core.Trace
         trace for the requested channel
     """
-    if interval == 'second':
+    if interval == 'tenhertz':
+        delta = 0.1
+    elif interval == 'second':
         delta = 1.
     elif interval == 'minute':
         delta = 60.
-    elif interval == 'hourly':
+    elif interval == 'hour':
         delta = 3600.
-    elif interval == 'daily':
+    elif interval == 'day':
         delta = 86400.
     stats = obspy.core.Stats()
     stats.network = network
diff --git a/geomagio/edge/EdgeFactory.py b/geomagio/edge/EdgeFactory.py
index dd8e13866d480cd8634418fabea32f14f536605a..9b672df4d95b40abdba97306be47d1f37bdd8d7d 100644
--- a/geomagio/edge/EdgeFactory.py
+++ b/geomagio/edge/EdgeFactory.py
@@ -51,7 +51,7 @@ class EdgeFactory(TimeseriesFactory):
     type: str
         the data type {variation, quasi-definitive, definitive}
     interval: str
-        the data interval {daily, hourly, minute, second}
+        the data interval {day, hour, minute, second}
     observatoryMetadata: ObservatoryMetadata object
         an ObservatoryMetadata object used to replace the default
         ObservatoryMetadata.
@@ -115,7 +115,7 @@ class EdgeFactory(TimeseriesFactory):
             list of channels to load
         type: {'variation', 'quasi-definitive', 'definitive'}
             data type.
-        interval: {'daily', 'hourly', 'minute', 'second'}
+        interval: {'day', 'hour', 'minute', 'second', 'tenhertz'}
             data interval.
 
         Returns
@@ -175,7 +175,7 @@ class EdgeFactory(TimeseriesFactory):
             list of channels to load
         type: {'variation', 'quasi-definitive', 'definitive'}
             data type.
-        interval: {'daily', 'hourly', 'minute', 'second'}
+        interval: {'day', 'hour', 'minute', 'second', 'tenhertz'}
             data interval.
 
         Notes
@@ -427,9 +427,9 @@ class EdgeFactory(TimeseriesFactory):
         interval type
         """
         interval_code = None
-        if interval == 'daily':
+        if interval == 'day':
             interval_code = 'D'
-        elif interval == 'hourly':
+        elif interval == 'hour':
             interval_code = 'H'
         elif interval == 'minute':
             interval_code = 'M'
@@ -530,7 +530,7 @@ class EdgeFactory(TimeseriesFactory):
             channel to load
         type: {'variation', 'quasi-definitive', 'definitive'}
             data type.
-        interval: {'daily', 'hourly', 'minute', 'second'}
+        interval: {'day', 'hour', 'minute', 'second', 'tenhertz'}
             data interval.
         starttime: obspy.core.UTCDateTime
         endtime: obspy.core.UTCDateTime
diff --git a/geomagio/edge/MiniSeedFactory.py b/geomagio/edge/MiniSeedFactory.py
new file mode 100644
index 0000000000000000000000000000000000000000..bced26bba9e57bdaf87e01188993135b6b30b899
--- /dev/null
+++ b/geomagio/edge/MiniSeedFactory.py
@@ -0,0 +1,518 @@
+"""Factory that loads data from earthworm and writes to Edge.
+
+EdgeFactory uses obspy earthworm class to read data from any
+earthworm standard Waveserver using the obspy getWaveform call.
+
+Writing will be implemented with Edge specific capabilities,
+to take advantage of it's newer realtime abilities.
+
+Edge is the USGS earthquake hazard centers replacement for earthworm.
+"""
+from __future__ import absolute_import
+
+import sys
+from io import BytesIO
+import numpy
+import numpy.ma
+
+import obspy.core
+from obspy.clients.neic import client as miniseed
+
+from .. import ChannelConverter, TimeseriesUtility
+from ..TimeseriesFactory import TimeseriesFactory
+from ..TimeseriesFactoryException import TimeseriesFactoryException
+from ..ObservatoryMetadata import ObservatoryMetadata
+
+
+class MiniSeedFactory(TimeseriesFactory):
+    """TimeseriesFactory for Edge related data.
+
+    Parameters
+    ----------
+    host: str
+        a string representing the IP number of the host to connect to.
+    port: integer
+        the port number the miniseed query server is listening on.
+    observatory: str
+        the observatory code for the desired observatory.
+    channels: array
+        an array of channels {H, D, E, F, Z, MGD, MSD, HGD}.
+        Known since channel names are mapped based on interval and type,
+        others are passed through, see #_get_edge_channel().
+    type: str
+        the data type {variation, quasi-definitive, definitive}
+    interval: str
+        the data interval {'day', 'hour', 'minute', 'second', 'tenhertz'}
+    observatoryMetadata: ObservatoryMetadata object
+        an ObservatoryMetadata object used to replace the default
+        ObservatoryMetadata.
+    locationCode: str
+        the location code for the given edge server, overrides type
+        in get_timeseries/put_timeseries
+
+    See Also
+    --------
+    TimeseriesFactory
+
+    Notes
+    -----
+    This is designed to read from any earthworm style waveserver, but it
+        currently only writes to an edge. Edge mimics an earthworm style
+        waveserver close enough that we hope to maintain that compatibility
+        for reading.
+    """
+
+    def __init__(self, host='cwbpub.cr.usgs.gov', port=2061, write_port=7981,
+            observatory=None, channels=None, type=None, interval=None,
+            observatoryMetadata=None, locationCode=None):
+        TimeseriesFactory.__init__(self, observatory, channels, type, interval)
+
+        self.client = miniseed.Client(host, port)
+
+        self.observatoryMetadata = observatoryMetadata or ObservatoryMetadata()
+        self.locationCode = locationCode
+        self.interval = interval
+        self.host = host
+        self.port = port
+        self.write_port = write_port
+
+    def get_timeseries(self, starttime, endtime, observatory=None,
+            channels=None, type=None, interval=None):
+        """Get timeseries data
+
+        Parameters
+        ----------
+        starttime: obspy.core.UTCDateTime
+            time of first sample.
+        endtime: obspy.core.UTCDateTime
+            time of last sample.
+        observatory: str
+            observatory code.
+        channels: array_like
+            list of channels to load
+        type: {'variation', 'quasi-definitive', 'definitive'}
+            data type.
+        interval: {'day', 'hour', 'minute', 'second', 'tenhertz'}
+            data interval.
+
+        Returns
+        -------
+        obspy.core.Stream
+            timeseries object with requested data.
+
+        Raises
+        ------
+        TimeseriesFactoryException
+            if invalid values are requested, or errors occur while
+            retrieving timeseries.
+        """
+        observatory = observatory or self.observatory
+        channels = channels or self.channels
+        type = type or self.type
+        interval = interval or self.interval
+
+        if starttime > endtime:
+            raise TimeseriesFactoryException(
+                'Starttime before endtime "%s" "%s"' % (starttime, endtime))
+
+        # need this until https://github.com/obspy/obspy/pull/1179
+        # replace stdout
+        original_stdout = sys.stdout
+        temp_stdout = BytesIO()
+        try:
+            sys.stdout = temp_stdout
+            # get the timeseries
+            timeseries = obspy.core.Stream()
+            for channel in channels:
+                data = self._get_timeseries(starttime, endtime, observatory,
+                        channel, type, interval)
+                timeseries += data
+        # restore stdout
+        finally:
+            output = temp_stdout.getvalue()
+            if output:
+                sys.stderr.write(str(output))
+            temp_stdout.close()
+            sys.stdout = original_stdout
+        self._post_process(timeseries, starttime, endtime, channels)
+
+        return timeseries
+
+    def put_timeseries(self, timeseries, starttime=None, endtime=None,
+                observatory=None, channels=None, type=None, interval=None):
+        """Put timeseries data
+
+        Parameters
+        ----------
+        timeseries: obspy.core.Stream
+            timeseries object with data to be written
+        observatory: str
+            observatory code.
+        channels: array_like
+            list of channels to load
+        type: {'variation', 'quasi-definitive', 'definitive'}
+            data type.
+        interval: {'day', 'hour', 'minute', 'second', 'tenhertz'}
+            data interval.
+
+        Notes
+        -----
+        Streams sent to timeseries are expected to have a single trace per
+            channel and that trace should have an ndarray, with nan's
+            representing gaps.
+        """
+        stats = timeseries[0].stats
+        observatory = observatory or stats.station or self.observatory
+        channels = channels or self.channels
+        type = type or self.type or stats.data_type
+        interval = interval or self.interval or stats.data_interval
+
+        if (starttime is None or endtime is None):
+            starttime, endtime = TimeseriesUtility.get_stream_start_end_times(
+                    timeseries)
+        for channel in channels:
+            if timeseries.select(channel=channel).count() == 0:
+                raise TimeseriesFactoryException(
+                    'Missing channel "%s" for output, available channels %s' %
+                    (channel, str(TimeseriesUtility.get_channels(timeseries))))
+        for channel in channels:
+            self._put_channel(timeseries, observatory, channel, type,
+                    interval, starttime, endtime)
+
+    def _convert_stream_to_masked(self, timeseries, channel):
+        """convert geomag edge traces in a timeseries stream to a MaskedArray
+            This allows for gaps and splitting.
+        Parameters
+        ----------
+        stream : obspy.core.stream
+            a stream retrieved from a geomag edge representing one channel.
+        channel: string
+            the channel to be masked.
+        Returns
+        -------
+        obspy.core.stream
+            a stream with all traces converted to masked arrays.
+        """
+        stream = timeseries.copy()
+        for trace in stream.select(channel=channel):
+            trace.data = numpy.ma.masked_invalid(trace.data)
+        return stream
+
+    def _get_edge_channel(self, observatory, channel, type, interval):
+        """get edge channel.
+
+        Parameters
+        ----------
+        observatory : str
+            observatory code
+        channel : str
+            single character channel {H, E, D, Z, F, X, Y, G} or
+            any appropriate edge channel, ie MSD, MGD, HGD.
+        type : str
+            data type {definitive, quasi-definitive, variation}
+        interval : str
+            interval length {'day', 'hour', 'minute', 'second', 'tenhertz'}
+
+        Returns
+        -------
+        edge_channel
+            {MVH, MVE, MVD, MGD etc}
+        """
+        edge_interval_code = self._get_interval_code(interval)
+        edge_channel = None
+
+        # If form is chan.loc, return chan (left) portion.
+        # Allows specific chan/loc selection.
+        if channel.find('.') >= 0:
+            tmplist = channel.split('.')
+            return tmplist[0].strip()
+
+        # see if channel name uses _ for ELEMENT_SUFFIX
+        element = None
+        suffix = None
+        if channel.find('_') >= 0:
+            element, suffix = channel.split('_')
+
+        # 10Hz should be bin/volt
+        if interval == 'tenhertz':
+            middle = None
+            if suffix == 'Bin':
+                middle = 'Y'
+            elif suffix == 'Volt':
+                middle = 'E'
+            elif suffix is not None:
+                raise TimeseriesFactoryException(
+                        'bad channel suffix "%s", wanted "Bin" or "Volt"'
+                        % suffix)
+            # check for expected channels
+            if element in ('U', 'V', 'W') and middle is not None:
+                return edge_interval_code + middle + element
+            else:
+                # unknown, assume advanced user
+                return channel
+
+        if suffix is not None:
+            if suffix == 'Dist' or suffix == 'SQ' or suffix == 'SV':
+                # these suffixes modify location code, but use element channel
+                channel = element
+            else:
+                raise TimeseriesFactoryException(
+                        'bad channel suffix "%s", wanted "Dist", "SQ", or "SV"'
+                        % suffix)
+        if channel in ('D', 'F', 'G', 'H', 'U', 'V', 'W', 'X', 'Y', 'Z'):
+            # normal elements
+            edge_channel = edge_interval_code + 'F' + channel
+        elif channel == 'E-E':
+            edge_channel = edge_interval_code + 'QE'
+        elif channel == 'E-N':
+            edge_channel = edge_interval_code + 'QN'
+        elif channel == 'Dst4':
+            edge_channel = edge_interval_code + 'X4'
+        elif channel == 'Dst3':
+            edge_channel = edge_interval_code + 'X3'
+        else:
+            edge_channel = channel
+        return edge_channel
+
+    def _get_edge_location(self, observatory, channel, data_type, interval):
+        """get edge location.
+
+        The edge location code is currently determined by the type
+            passed in.
+
+        Parameters
+        ----------
+        observatory : str
+            observatory code
+        channel : str
+            single character channel {H, E, D, Z, F}
+        data_type : str
+            data type {definitive, quasi-definitive, variation}
+        interval : str
+            interval length {'day', 'hour', 'minute', 'second', 'tenhertz'}
+
+        Returns
+        -------
+        location
+            returns an edge location code
+        """
+        # If form is chan.loc, return loc (right) portion
+        # Allows specific chan/loc selection.
+        if channel.find('.') >= 0:
+            tmplist = channel.split('.')
+            return tmplist[1].strip()
+        # factory override
+        if self.locationCode is not None:
+            return self.locationCode
+        # determine prefix
+        location_prefix = 'R'
+        if data_type == 'variation' or data_type == 'reported':
+            location_prefix = 'R'
+        elif data_type == 'adjusted' or data_type == 'provisional':
+            location_prefix = 'A'
+        elif data_type == 'quasi-definitive':
+            location_prefix = 'Q'
+        elif data_type == 'definitive':
+            location_prefix = 'D'
+        # determine suffix
+        location_suffix = '0'
+        if channel.find('_') >= 0:
+            _, suffix = channel.split('_')
+            if suffix == 'Dist':
+                location_suffix = 'D'
+            elif suffix == 'SQ':
+                location_suffix = 'Q'
+            elif suffix == 'SV':
+                location_suffix = 'V'
+            elif suffix not in ('Bin', 'Volt'):
+                raise TimeseriesFactoryException(
+                        'bad channel suffix "%s", wanted "Dist", "SQ", or "SV"'
+                        % suffix)
+        return location_prefix + location_suffix
+
+    def _get_edge_network(self, observatory, channel, type, interval):
+        """get edge network code.
+
+        Parameters
+        ----------
+        observatory : str
+            observatory code
+        channel : str
+            single character channel {H, E, D, Z, F}
+        type : str
+            data type {definitive, quasi-definitive, variation}
+        interval : str
+            interval length {'day', 'hour', 'minute', 'second', 'tenhertz'}
+
+        Returns
+        -------
+        network
+            always NT
+        """
+        return 'NT'
+
+    def _get_edge_station(self, observatory, channel, type, interval):
+        """get edge station.
+
+        Parameters
+        ----------
+        observatory : str
+            observatory code
+        channel : str
+            single character channel {H, E, D, Z, F}
+        type : str
+            data type {definitive, quasi-definitive, variation}
+        interval : str
+            interval length {'day', 'hour', 'minute', 'second', 'tenhertz'}
+
+        Returns
+        -------
+        station
+            the observatory is returned as the station
+        """
+        return observatory
+
+    def _get_interval_code(self, interval):
+        """get edge interval code.
+
+        Converts the metadata interval string, into an edge single character
+            edge code.
+
+        Parameters
+        ----------
+        interval : str
+            interval length {'day', 'hour', 'minute', 'second', 'tenhertz'}
+
+        Returns
+        -------
+        interval type
+        """
+        interval_code = None
+        if interval == 'day':
+            interval_code = 'P'
+        elif interval == 'hour':
+            interval_code = 'R'
+        elif interval == 'minute':
+            interval_code = 'U'
+        elif interval == 'second':
+            interval_code = 'L'
+        elif interval == 'tenhertz':
+            interval_code = 'B'
+        else:
+            raise TimeseriesFactoryException(
+                    'Unexpected interval "%s"' % interval)
+        return interval_code
+
+    def _get_timeseries(self, starttime, endtime, observatory,
+                channel, type, interval):
+        """get timeseries data for a single channel.
+
+        Parameters
+        ----------
+        starttime: obspy.core.UTCDateTime
+            the starttime of the requested data
+        endtime: obspy.core.UTCDateTime
+            the endtime of the requested data
+        observatory : str
+            observatory code
+        channel : str
+            single character channel {H, E, D, Z, F}
+        type : str
+            data type {definitive, quasi-definitive, variation}
+        interval : str
+            interval length {'day', 'hour', 'minute', 'second', 'tenhertz'}
+
+        Returns
+        -------
+        obspy.core.trace
+            timeseries trace of the requested channel data
+        """
+        station = self._get_edge_station(observatory, channel,
+                type, interval)
+        location = self._get_edge_location(observatory, channel,
+                type, interval)
+        network = self._get_edge_network(observatory, channel,
+                type, interval)
+        edge_channel = self._get_edge_channel(observatory, channel,
+                type, interval)
+        data = self.client.get_waveforms(network, station, location,
+                edge_channel, starttime, endtime)
+        data.merge()
+        if data.count() == 0:
+            data += TimeseriesUtility.create_empty_trace(
+                starttime, endtime, observatory, channel, type,
+                interval, network, station, location)
+        self._set_metadata(data,
+                observatory, channel, type, interval)
+        return data
+
+    def _post_process(self, timeseries, starttime, endtime, channels):
+        """Post process a timeseries stream after the raw data is
+                is fetched from querymom. Specifically changes
+                any MaskedArray to a ndarray with nans representing gaps.
+                Then calls pad_timeseries to deal with gaps at the
+                beggining or end of the streams.
+
+        Parameters
+        ----------
+        timeseries: obspy.core.stream
+            The timeseries stream as returned by the call to get_waveforms
+        starttime: obspy.core.UTCDateTime
+            the starttime of the requested data
+        endtime: obspy.core.UTCDateTime
+            the endtime of the requested data
+        channels: array_like
+            list of channels to load
+
+        Notes: the original timeseries object is changed.
+        """
+        for trace in timeseries:
+            if isinstance(trace.data, numpy.ma.MaskedArray):
+                trace.data.set_fill_value(numpy.nan)
+                trace.data = trace.data.filled()
+
+        if 'D' in channels:
+            for trace in timeseries.select(channel='D'):
+                trace.data = ChannelConverter.get_radians_from_minutes(
+                    trace.data)
+
+        TimeseriesUtility.pad_timeseries(timeseries, starttime, endtime)
+
+    def _put_channel(self, timeseries, observatory, channel, type, interval,
+                starttime, endtime):
+        """Put a channel worth of data
+
+        Parameters
+        ----------
+        timeseries: obspy.core.Stream
+            timeseries object with data to be written
+        observatory: str
+            observatory code.
+        channel: str
+            channel to load
+        type: {'variation', 'quasi-definitive', 'definitive'}
+            data type.
+        interval: {'day', 'hour', 'minute', 'second', 'tenhertz'}
+            data interval.
+        starttime: obspy.core.UTCDateTime
+        endtime: obspy.core.UTCDateTime
+        """
+        raise NotImplementedError('"_put_channel" not implemented')
+
+    def _set_metadata(self, stream, observatory, channel, type, interval):
+        """set metadata for a given stream/channel
+        Parameters
+        ----------
+        observatory : str
+            observatory code
+        channel : str
+            edge channel code {MVH, MVE, MVD, ...}
+        type : str
+            data type {definitive, quasi-definitive, variation}
+        interval : str
+            interval length {'day', 'hour', 'minute', 'second', 'tenhertz'}
+        """
+
+        for trace in stream:
+            self.observatoryMetadata.set_metadata(trace.stats, observatory,
+                    channel, type, interval)
diff --git a/geomagio/edge/__init__.py b/geomagio/edge/__init__.py
index 589bc42bfd74a03997ff1188f08eb5752ab1903d..047575c7d19fef9217a5a2784eec76991c92d8e6 100644
--- a/geomagio/edge/__init__.py
+++ b/geomagio/edge/__init__.py
@@ -4,10 +4,12 @@ from __future__ import absolute_import
 
 from .EdgeFactory import EdgeFactory
 from .LocationCode import LocationCode
+from .MiniSeedFactory import MiniSeedFactory
 from .RawInputClient import RawInputClient
 
 __all__ = [
     'EdgeFactory',
     'LocationCode',
+    'MiniSeedFactory',
     'RawInputClient'
 ]
diff --git a/test/edge_test/EdgeFactory_test.py b/test/edge_test/EdgeFactory_test.py
index e0b14e0e803e52de250db196d9eab653a3ec2080..e7a731d3446c75853f94bcd57ff6fed185f7d470 100644
--- a/test/edge_test/EdgeFactory_test.py
+++ b/test/edge_test/EdgeFactory_test.py
@@ -58,8 +58,8 @@ def test__get_edge_location():
 def test__get_interval_code():
     """edge_test.EdgeFactory_test.test__get_interval_code()
     """
-    assert_equals(EdgeFactory()._get_interval_code('daily'), 'D')
-    assert_equals(EdgeFactory()._get_interval_code('hourly'), 'H')
+    assert_equals(EdgeFactory()._get_interval_code('day'), 'D')
+    assert_equals(EdgeFactory()._get_interval_code('hour'), 'H')
     assert_equals(EdgeFactory()._get_interval_code('minute'), 'M')
     assert_equals(EdgeFactory()._get_interval_code('second'), 'S')
 
diff --git a/test/edge_test/MiniSeedFactory_test.py b/test/edge_test/MiniSeedFactory_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..dce141ee1cfd03fa3ee0b0b39899cc99935c014d
--- /dev/null
+++ b/test/edge_test/MiniSeedFactory_test.py
@@ -0,0 +1,92 @@
+"""Tests for MiniSeedFactory.py"""
+
+from obspy.core import Stream, Trace, UTCDateTime
+from geomagio.edge import MiniSeedFactory
+from nose.tools import assert_equals
+
+
+def test__get_edge_network():
+    """edge_test.MiniSeedFactory_test.test__get_edge_network()
+    """
+    # _get_edge_network should always return NT for use by USGS geomag
+    assert_equals(
+            MiniSeedFactory()._get_edge_network(' ', ' ', ' ', ' '),
+            'NT')
+
+
+def test__get_edge_station():
+    """edge_test.MiniSeedFactory_test.test__get_edge_station()
+    """
+    # _get_edge_station will return the observatory code passed in.
+    assert_equals(
+            MiniSeedFactory()._get_edge_station('BOU', ' ', ' ', ' '),
+            'BOU')
+
+
+def test__get_edge_channel():
+    """edge_test.MiniSeedFactory_test.test__get_edge_channel()
+    """
+    # Call private function _get_edge_channel, make certain
+    # it gets back the appropriate 2 character code.
+    factory = MiniSeedFactory()
+    assert_equals(factory._get_edge_channel('', 'D', '', 'minute'), 'UFD')
+    assert_equals(factory._get_edge_channel('', 'U', '', 'minute'), 'UFU')
+    assert_equals(factory._get_edge_channel('', 'F', '', 'minute'), 'UFF')
+    assert_equals(factory._get_edge_channel('', 'H', '', 'minute'), 'UFH')
+    assert_equals(factory._get_edge_channel('', 'BEU', '', 'minute'), 'BEU')
+    assert_equals(factory._get_edge_channel('', 'Dst4', '', 'minute'), 'UX4')
+    assert_equals(factory._get_edge_channel('', 'Dst3', '', 'minute'), 'UX3')
+    assert_equals(factory._get_edge_channel('', 'E-E', '', 'minute'), 'UQE')
+    assert_equals(factory._get_edge_channel('', 'E-N', '', 'minute'), 'UQN')
+
+
+def test__get_edge_location():
+    """edge_test.MiniSeedFactory_test.test__get_edge_location()
+    """
+    # Call _get_edge_location, make certain it returns the correct edge
+    # location code.
+    assert_equals(MiniSeedFactory()._get_edge_location(
+            '', '', 'variation', ''), 'R0')
+    assert_equals(MiniSeedFactory()._get_edge_location(
+            '', '', 'quasi-definitive', ''), 'Q0')
+    assert_equals(MiniSeedFactory()._get_edge_location(
+            '', '', 'definitive', ''), 'D0')
+
+
+def test__get_interval_code():
+    """edge_test.MiniSeedFactory_test.test__get_interval_code()
+    """
+    assert_equals(MiniSeedFactory()._get_interval_code('day'), 'P')
+    assert_equals(MiniSeedFactory()._get_interval_code('hour'), 'R')
+    assert_equals(MiniSeedFactory()._get_interval_code('minute'), 'U')
+    assert_equals(MiniSeedFactory()._get_interval_code('second'), 'L')
+    assert_equals(MiniSeedFactory()._get_interval_code('tenhertz'), 'B')
+
+
+def test__set_metadata():
+    """edge_test.MiniSeedFactory_test.test__set_metadata()
+    """
+    # Call _set_metadata with 2 traces,  and make certain the stats get
+    # set for both traces.
+    trace1 = Trace()
+    trace2 = Trace()
+    stream = Stream(traces=[trace1, trace2])
+    MiniSeedFactory()._set_metadata(stream, 'BOU', 'H', 'variation', 'minute')
+    assert_equals(stream[0].stats['channel'], 'H')
+    assert_equals(stream[1].stats['channel'], 'H')
+
+
+# def test_get_timeseries():
+def dont_get_timeseries():
+    """edge_test.MiniSeedFactory_test.test_get_timeseries()"""
+    # Call get_timeseries, and test stats for comfirmation that it came back.
+    # TODO, need to pass in host and port from a config file, or manually
+    #   change for a single test.
+    edge_factory = MiniSeedFactory(host='TODO', port='TODO')
+    timeseries = edge_factory.get_timeseries(
+        UTCDateTime(2015, 3, 1, 0, 0, 0), UTCDateTime(2015, 3, 1, 1, 0, 0),
+        'BOU', ('H'), 'variation', 'minute')
+    assert_equals(timeseries.select(channel='H')[0].stats.station,
+        'BOU', 'Expect timeseries to have stats')
+    assert_equals(timeseries.select(channel='H')[0].stats.channel,
+        'H', 'Expect timeseries stats channel to be equal to H')