diff --git a/etc/adjusted/BOU201601vmin.min b/etc/adjusted/BOU201601vmin.min
index 57c20c44233ae76dfd9dae307ce91c14086b4651..83ef32f4830f78ffc089e167dc6230832db4e2ee 100644
--- a/etc/adjusted/BOU201601vmin.min
+++ b/etc/adjusted/BOU201601vmin.min
@@ -5,7 +5,7 @@
  Geodetic Latitude      40.137                                       |
  Geodetic Longitude     254.764                                      |
  Elevation              1682                                         |
- Reported               HEZF                                         |
+ Reported               UVWF                                         |
  Sensor Orientation     HDZF                                         |
  Digital Sampling       100.0 second                                 |
  Data Interval Type     filtered 1-minute (00:15-01:45)              |
@@ -19,7 +19,7 @@
  # CONDITIONS OF USE: The Conditions of Use for data provided        |
  # through INTERMAGNET and acknowledgement templates can be found at |
  # www.intermagnet.org                                               |
-DATE       TIME         DOY     BOUH      BOUE      BOUZ      BOUF   |
+DATE       TIME         DOY     BOUU      BOUV      BOUW      BOUF   |
 2016-01-01 00:00:00.000 001     20735.93    -99.77  47370.21  52248.63
 2016-01-01 00:01:00.000 001     20735.48    -95.30  47370.53  52248.72
 2016-01-01 00:02:00.000 001     20736.30    -91.25  47371.05  52249.49
diff --git a/etc/adjusted/BOU202005adj.min b/etc/adjusted/BOU202005adj.min
index 55f8fcc64580668ad815ae8706c1628cbbea0285..551fd3ed7ec4fabb3ecf4b5a360b782ce4f45b3c 100644
--- a/etc/adjusted/BOU202005adj.min
+++ b/etc/adjusted/BOU202005adj.min
@@ -5,7 +5,7 @@
  Geodetic Latitude      40.137                                       |
  Geodetic Longitude     254.763                                      |
  Elevation              1682                                         |
- Reported               HENULNUL                                     |
+ Reported               UVNULNUL                                     |
  Sensor Orientation     HDZF                                         |
  Digital Sampling       0.01 second                                  |
  Data Interval Type     filtered 1-minute (00:15-01:45)              |
@@ -17,7 +17,7 @@
  # CONDITIONS OF USE: The Conditions of Use for data provided        |
  # through INTERMAGNET and acknowledgement templates can be found at |
  # www.intermagnet.org                                               |
-DATE       TIME         DOY     BOUH      BOUE      BOUNUL    BOUNUL |
+DATE       TIME         DOY     BOUU      BOUV      BOUNUL    BOUNUL |
 2020-05-20 16:15:00.000 141    -20799.04     80.92  99999.00  99999.00
 2020-05-20 16:16:00.000 141    -20798.26     79.97  99999.00  99999.00
 2020-05-20 16:17:00.000 141    -20799.43     81.22  99999.00  99999.00
diff --git a/etc/adjusted/BOU202005vmin.min b/etc/adjusted/BOU202005vmin.min
index 545671c9b844f2f36fa64467d195b52c2780c762..f3d0f15bc47bdb0d77d374942c155d20e95955ee 100644
--- a/etc/adjusted/BOU202005vmin.min
+++ b/etc/adjusted/BOU202005vmin.min
@@ -5,7 +5,7 @@
  Geodetic Latitude      40.137                                       |
  Geodetic Longitude     254.763                                      |
  Elevation              1682                                         |
- Reported               HENULNUL                                     |
+ Reported               UVNULNUL                                     |
  Sensor Orientation     HDZF                                         |
  Digital Sampling       0.01 second                                  |
  Data Interval Type     filtered 1-minute (00:15-01:45)              |
@@ -19,7 +19,7 @@
  # CONDITIONS OF USE: The Conditions of Use for data provided        |
  # through INTERMAGNET and acknowledgement templates can be found at |
  # www.intermagnet.org                                               |
-DATE       TIME         DOY     BOUH      BOUE      BOUNUL    BOUNUL |
+DATE       TIME         DOY     BOUU      BOUV      BOUNUL    BOUNUL |
 2020-05-20 16:15:00.000 141     20799.04    -80.92  99999.00  99999.00
 2020-05-20 16:16:00.000 141     20798.26    -79.97  99999.00  99999.00
 2020-05-20 16:17:00.000 141     20799.43    -81.22  99999.00  99999.00
diff --git a/geomagio/ChannelConverter.py b/geomagio/ChannelConverter.py
index a7c3bfc8b255f77521bce41916ab0b7484a02693..3884af3d286050fba482a6eee352432d87c94c9f 100644
--- a/geomagio/ChannelConverter.py
+++ b/geomagio/ChannelConverter.py
@@ -5,7 +5,7 @@ the geomagnetic community.
 We use three coordinate systems.
 Geo: Based on Geographic North.  X, Y, Z
      X is north, Y is east
-Obs: Based on the observatories orientaion. H, E, Z [d]
+Obs: Based on the observatories orientaion. U, V, W [d]
 Mag: Based on Magnetic North. H, D, Z [E]
 
 d0: Declination baseline in radians
@@ -29,15 +29,15 @@ R2M = 180.0 / numpy.pi * 60  # Radians to Minutes
 # ###
 
 
-def get_geo_from_obs(h, e, d0=0):
+def get_geo_from_obs(u, v, d0=0):
     """gets the geographical components given the observatory components.
 
     Parameters
     __________
-    h: array_like
-        the h component from the observatory
-    e: array_like
-        the e component from the observatory
+    u: array_like
+        the u component from the observatory
+    v: array_like
+        the v component from the observatory
     d0: float
         the declination baseline angle in radians
 
@@ -47,7 +47,7 @@ def get_geo_from_obs(h, e, d0=0):
         [0]: x component as a float
         [1]: y component as a float
     """
-    mag_h, mag_d = get_mag_from_obs(h, e, d0)
+    mag_h, mag_d = get_mag_from_obs(u, v, d0)
     return get_geo_from_mag(mag_h, mag_d)
 
 
@@ -112,15 +112,15 @@ def get_geo_y_from_mag(h, d):
 # ###
 # get magnetic north coordinates from....
 # ###
-def get_mag_from_obs(h, e, d0=0):
+def get_mag_from_obs(u, v, d0=0):
     """gets the magnetic north components given the observatory components.
 
     Parameters
     __________
-    h: array_like
-        the h component from the observatory
-    e: array_like
-        the e component from the observatory
+    u: array_like
+        the u component from the observatory
+    v: array_like
+        the v component from the observatory
     d0: float
         the declination baseline angle in radians
 
@@ -130,8 +130,8 @@ def get_mag_from_obs(h, e, d0=0):
         [0]: total h component as a float
         [1]: total d declination as a float
     """
-    mag_h = get_mag_h_from_obs(h, e)
-    mag_d = get_mag_d_from_obs(h, e, d0)
+    mag_h = get_mag_h_from_obs(u, v)
+    mag_d = get_mag_d_from_obs(u, v, d0)
     return (mag_h, mag_d)
 
 
@@ -156,15 +156,15 @@ def get_mag_from_geo(x, y):
     return (mag_h, mag_d)
 
 
-def get_mag_d_from_obs(h, e, d0=0):
+def get_mag_d_from_obs(u, v, d0=0):
     """gets the magnetic d component given the observatory components.
 
     Parameters
     __________
-    h: array_like
-        the h component from the observatory
-    e: array_like
-        the e component from the observatory
+    u: array_like
+        the u component from the observatory
+    v: array_like
+        the v component from the observatory
     d0: float
         the declination baseline angle in radians
 
@@ -173,7 +173,7 @@ def get_mag_d_from_obs(h, e, d0=0):
     array_like
         the total magnetic declination
     """
-    return numpy.add(d0, get_obs_d_from_obs(h, e))
+    return numpy.add(d0, get_obs_d_from_obs(u, v))
 
 
 def get_mag_d_from_geo(x, y):
@@ -194,22 +194,22 @@ def get_mag_d_from_geo(x, y):
     return numpy.arctan2(y, x)
 
 
-def get_mag_h_from_obs(h, e):
+def get_mag_h_from_obs(u, v):
     """gets the magnetic h component given the observatory components.
 
     Parameters
     __________
-    h: array_like
-        the h component from the observatory
-    e: array_like
-        the e component from the observatory
+    u: array_like
+        the u component from the observatory
+    v: array_like
+        the v component from the observatory
 
     Returns
     _______
     array_like
         the total magnetic h component
     """
-    return numpy.hypot(h, e)
+    return numpy.hypot(u, v)
 
 
 def get_mag_h_from_geo(x, y):
@@ -271,32 +271,32 @@ def get_obs_from_mag(h, d, d0=0):
      Returns
     _______
     tuple of array_like
-        [0]: observatory h component
-        [1]: observatory e component
+        [0]: observatory u component
+        [1]: observatory v component
         [2]: observatory d declination
     """
-    obs_h = get_obs_h_from_mag(h, d, d0)
-    obs_e = get_obs_e_from_mag(h, d, d0)
-    return (obs_h, obs_e)
+    obs_u = get_obs_u_from_mag(h, d, d0)
+    obs_v = get_obs_v_from_mag(h, d, d0)
+    return (obs_u, obs_v)
 
 
 # inividual get obs from calls
-def get_obs_d_from_obs(h, e):
+def get_obs_d_from_obs(u, v):
     """gets the observatory d declination given the observatory components.
 
     Parameters
     __________
-    h: array_like
-        the h component from the observatory
-    e: array_like
-        the e component from the observatory
+    u: array_like
+        the u component from the observatory
+    v: array_like
+        the v component from the observatory
 
     Returns
     _______
     array_like
         the observatory d declination
     """
-    return numpy.arctan2(e, h)
+    return numpy.arctan2(v, u)
 
 
 def get_obs_d_from_mag_d(d, d0=0):
@@ -318,8 +318,8 @@ def get_obs_d_from_mag_d(d, d0=0):
     return numpy.subtract(d, d0)
 
 
-def get_obs_e_from_mag(h, d, d0=0):
-    """gets the observatory e component given the magnetic components.
+def get_obs_v_from_mag(h, d, d0=0):
+    """gets the observatory v component given the magnetic components.
 
     Parameters
     __________
@@ -333,32 +333,32 @@ def get_obs_e_from_mag(h, d, d0=0):
     Returns
     _______
     array_like
-        the observatory e component
+        the observatory v component
     """
     obs_d = get_obs_d_from_mag_d(d, d0)
     return numpy.multiply(h, numpy.sin(obs_d))
 
 
-def get_obs_e_from_obs(h, d):
-    """gets the observatory e component given the observatory components.
+def get_obs_v_from_obs(h, d):
+    """gets the observatory v component given the observatory components.
 
     Parameters
     __________
-    h: array_like
-        the observatory h component.
+    u: array_like
+        the observatory u component.
     d: array_like
         the observatory d declination.
 
     Returns
     _______
     array_like
-        the observatory e component
+        the observatory v component
     """
     return numpy.multiply(h, numpy.tan(d))
 
 
-def get_obs_h_from_mag(h, d, d0=0):
-    """gets the observatory h component given the magnetic north components
+def get_obs_u_from_mag(h, d, d0=0):
+    """gets the observatory u component given the magnetic north components
 
     Parameters
     __________
@@ -372,7 +372,7 @@ def get_obs_h_from_mag(h, d, d0=0):
     Returns
     _______
     array_like
-        the observatory h component
+        the observatory u component
     """
     obs_d = get_obs_d_from_mag_d(d, d0)
     return numpy.multiply(h, numpy.cos(obs_d))
@@ -405,7 +405,7 @@ def get_computed_f_using_squares(x, y, z):
     Notes
     -----
     This works for geographic coordinates, or observatory coordinates.
-        ie x, y, z or h, e, z
+        ie x, y, z or u, v, w
         We're using variables x,y,z to represent generic cartisian coordinates.
     """
     x2 = numpy.square(x)
diff --git a/geomagio/StreamConverter.py b/geomagio/StreamConverter.py
index 4ef3232c5db1ef5b0ec7cf4eb62357a9d5550413..4677acee27626dc968216793895ba4102668b302 100644
--- a/geomagio/StreamConverter.py
+++ b/geomagio/StreamConverter.py
@@ -3,7 +3,7 @@
 We use three coordinate systems.
 Geo: Based on Geographic North.  X, Y, Z, F
      X is north, Y is east, Z is down
-Obs: Based on the observatories orientaion. H, E, Z, F, d0
+Obs: Based on the observatories orientaion. U, V, W, F, d0
 Mag: Based on Magnetic North. H, D, Z, F
 """
 from __future__ import absolute_import
@@ -48,7 +48,7 @@ def get_geo_from_obs(obs):
     Parameters
     ----------
     stream : obspy.core.Stream
-        stream containing observatory components H, D or E, Z, and F.
+        stream containing observatory components U, D or V, W, and F.
 
     Returns
     -------
@@ -86,20 +86,20 @@ def get_deltaf_from_obs(obs):
     Parameters
     ----------
     obs: obspy.core.Stream
-        stream containing the observatory components H, D or E, Z, and F.
+        stream containing the observatory components U, D or V, W, and F.
 
     Returns
     -------
     obspy.core.Stream
         stream object containing delta f values
     """
-    h = obs.select(channel="H")[0]
-    z = obs.select(channel="Z")[0]
+    u = obs.select(channel="U")[0]
+    w = obs.select(channel="W")[0]
     fs = obs.select(channel="F")[0]
-    e = __get_obs_e_from_obs(obs)
-    fv = ChannelConverter.get_computed_f_using_squares(h, e, z)
+    v = __get_obs_v_from_obs(obs)
+    fv = ChannelConverter.get_computed_f_using_squares(u, v, w)
     G = ChannelConverter.get_deltaf(fv, fs)
-    return obspy.core.Stream((__get_trace("G", h.stats, G),))
+    return obspy.core.Stream((__get_trace("G", u.stats, G),))
 
 
 def get_mag_from_geo(geo):
@@ -137,28 +137,28 @@ def get_mag_from_obs(obs):
     Parameters
     ----------
     obs : obspy.core.Stream
-        stream containing observatory components H, D or E, Z, and F.
+        stream containing observatory components U, D or V, W, and F.
 
     Returns
     -------
     obspy.core.Stream
         new stream object containing magnetic components H, D, Z, and F.
     """
-    h = obs.select(channel="H")[0]
-    e = __get_obs_e_from_obs(obs)
-    z = obs.select(channel="Z")
+    u = obs.select(channel="U")[0]
+    v = __get_obs_v_from_obs(obs)
+    w = obs.select(channel="W")
     f = obs.select(channel="F")
-    obs_h = h.data
-    obs_e = e.data
+    obs_u = u.data
+    obs_v = v.data
     d0 = ChannelConverter.get_radians_from_minutes(
-        numpy.float64(e.stats.declination_base) / 10
+        numpy.float64(v.stats.declination_base) / 10
     )
-    (mag_h, mag_d) = ChannelConverter.get_mag_from_obs(obs_h, obs_e, d0)
+    (mag_h, mag_d) = ChannelConverter.get_mag_from_obs(obs_u, obs_v, d0)
     return (
         obspy.core.Stream(
-            (__get_trace("H", h.stats, mag_h), __get_trace("D", e.stats, mag_d))
+            (__get_trace("H", u.stats, mag_h), __get_trace("D", v.stats, mag_d))
         )
-        + z
+        + w
         + f
     )
 
@@ -193,7 +193,7 @@ def get_obs_from_mag(mag, include_d=False):
     Returns
     -------
     obspy.core.Stream
-        new stream object containing observatory components H, D, E, Z, and F
+        new stream object containing observatory components U, D, V, W, and F
     """
     h = mag.select(channel="H")[0]
     d = mag.select(channel="D")[0]
@@ -205,43 +205,43 @@ def get_obs_from_mag(mag, include_d=False):
     d0 = ChannelConverter.get_radians_from_minutes(
         numpy.float64(d.stats.declination_base) / 10
     )
-    (obs_h, obs_e) = ChannelConverter.get_obs_from_mag(mag_h, mag_d, d0)
+    (obs_u, obs_v) = ChannelConverter.get_obs_from_mag(mag_h, mag_d, d0)
 
-    traces = (__get_trace("H", h.stats, obs_h), __get_trace("E", d.stats, obs_e))
+    traces = (__get_trace("U", h.stats, obs_u), __get_trace("V", d.stats, obs_v))
     if include_d:
-        obs_d = ChannelConverter.get_obs_d_from_obs(obs_h, obs_e)
+        obs_d = ChannelConverter.get_obs_d_from_obs(obs_u, obs_v)
         traces = traces + (__get_trace("D", d.stats, obs_d),)
     return obspy.core.Stream(traces) + z + f
 
 
-def get_obs_from_obs(obs, include_e=False, include_d=False):
+def get_obs_from_obs(obs, include_v=False, include_d=False):
     """Fill in the observatory parameters as requested
 
     Parameters
     ----------
     stream: obspy.core.Stream
-        stream containing the observatory components H, D or E, Z, and F.
+        stream containing the observatory components U, D or V, W, and F.
     include_e: boolean
-        whether to include the e component
+        whether to include the v component
     include_d: boolean
         whether to include the d component
 
     Returns
     -------
     obspy.core.Stream
-        new stream object containing observatory components H, D, E, Z, and F
+        new stream object containing observatory components U, D, V, W, and F
     """
-    h = obs.select(channel="H")[0]
-    z = obs.select(channel="Z")
+    u = obs.select(channel="U")[0]
+    w = obs.select(channel="W")
     f = obs.select(channel="F")
-    traces = (h,)
+    traces = (u,)
     if include_d:
         d = __get_obs_d_from_obs(obs)
         traces = traces + (d,)
-    if include_e:
-        e = __get_obs_e_from_obs(obs)
-        traces = traces + (e,)
-    return obspy.core.Stream(traces) + z + f
+    if include_v:
+        v = __get_obs_v_from_obs(obs)
+        traces = traces + (v,)
+    return obspy.core.Stream(traces) + w + f
 
 
 def __get_trace(channel, stats, data):
@@ -269,12 +269,12 @@ def __get_trace(channel, stats, data):
 def __get_obs_d_from_obs(obs):
     """Get trace containing observatory D component.
 
-    Returns D if found, otherwise computes D from H, E, D0.
+    Returns D if found, otherwise computes D from U, V, D0.
 
     Parameters
     ----------
     obs : obspy.core.Stream
-        observatory components (D) or (H, E).
+        observatory components (D) or (U, V).
 
     Returns
     -------
@@ -284,35 +284,35 @@ def __get_obs_d_from_obs(obs):
     try:
         d = obs.select(channel="D")[0]
     except IndexError:
-        h = obs.select(channel="H")[0]
-        e = obs.select(channel="E")[0]
+        u = obs.select(channel="U")[0]
+        v = obs.select(channel="V")[0]
         d = __get_trace(
-            "D", e.stats, ChannelConverter.get_obs_d_from_obs(h.data, e.data)
+            "D", v.stats, ChannelConverter.get_obs_d_from_obs(u.data, v.data)
         )
     return d
 
 
-def __get_obs_e_from_obs(obs):
-    """Get trace containing observatory E component.
+def __get_obs_v_from_obs(obs):
+    """Get trace containing observatory V component.
 
-    Returns E if found, otherwise computes E from H,D.
+    Returns V if found, otherwise computes V from U,D.
 
     Parameters
     ----------
     obs : obspy.core.Stream
-        observatory components (E) or (H, D).
+        observatory components (V) or (U, D).
 
     Returns
     -------
     obspy.core.Trace
-        observatory component E.
+        observatory component V.
     """
     try:
-        e = obs.select(channel="E")[0]
+        v = obs.select(channel="V")[0]
     except IndexError:
-        h = obs.select(channel="H")[0]
+        u = obs.select(channel="U")[0]
         d = obs.select(channel="D")[0]
-        e = __get_trace(
-            "E", d.stats, ChannelConverter.get_obs_e_from_obs(h.data, d.data)
+        v = __get_trace(
+            "V", d.stats, ChannelConverter.get_obs_v_from_obs(u.data, d.data)
         )
-    return e
+    return v
diff --git a/geomagio/TimeseriesFactory.py b/geomagio/TimeseriesFactory.py
index 7446f81ee6cc4877dde540044c53c742e9a3564f..c8294c82d67ff4a7992f0b1f1b05c2406354add1 100644
--- a/geomagio/TimeseriesFactory.py
+++ b/geomagio/TimeseriesFactory.py
@@ -1,11 +1,13 @@
 """Abstract Timeseries Factory Interface."""
 from __future__ import absolute_import, print_function
+from typing import List
 
 import numpy
-import obspy.core
+from obspy import Stream
 import os
 import sys
 from .TimeseriesFactoryException import TimeseriesFactoryException
+from . import derived
 from . import TimeseriesUtility
 from . import Util
 
@@ -59,6 +61,43 @@ class TimeseriesFactory(object):
         self.urlTemplate = urlTemplate
         self.urlInterval = urlInterval
 
+    def get_derived_timeseries(self, timeseries: Stream) -> Stream:
+        """compute missing derived channels"""
+        missing_channels = derived.get_missing_channels(timeseries=timeseries)
+        if not missing_channels:
+            return timeseries
+        input_timeseries = self.add_required_channels(
+            timeseries=timeseries, channels=missing_channels
+        )
+        output_channels = TimeseriesUtility.get_channels(stream=timeseries)
+        return derived.get_timeseries(
+            timeseries=input_timeseries, channels=output_channels
+        )
+
+    def add_required_channels(self, timeseries: Stream, channels: List[str]) -> Stream:
+        """adds required channels into timeseries for derived products"""
+        required_channels = derived.get_required_channels(
+            channels=channels, data_type=timeseries[0].stats.data_type
+        )
+        output_stream = Stream()
+        stats = timeseries[0].stats
+        defined_channels = TimeseriesUtility.get_channels(
+            stream=timeseries, contains_data=True
+        )
+        for channel in required_channels:
+            if channel in defined_channels:
+                output_stream += timeseries.select(channel=channel)[0]
+            output_stream += self.get_timeseries(
+                starttime=stats.starttime,
+                endtime=stats.endtime,
+                observatory=stats.station,
+                channels=(channel,),
+                type=stats.data_type,
+                interval=TimeseriesUtility.get_interval_from_delta(stats.delta),
+                derive_missing=False,
+            )
+        return output_stream
+
     def get_timeseries(
         self,
         starttime,
@@ -67,7 +106,8 @@ class TimeseriesFactory(object):
         channels=None,
         type=None,
         interval=None,
-    ):
+        derive_missing: bool = True,
+    ) -> Stream:
         """Get timeseries data.
 
         Support for specific channels, types, and intervals varies
@@ -93,11 +133,8 @@ class TimeseriesFactory(object):
         interval : {'day', 'hour', 'minute', 'month', 'second'}
             data interval, optional.
             uses default if unspecified.
-
-        Returns
-        -------
-        obspy.core.Stream
-            stream containing traces for requested timeseries.
+        derive_missing
+            if True, calculates trivial derived products missing from initial request
 
         Raises
         ------
@@ -109,7 +146,7 @@ class TimeseriesFactory(object):
         type = type or self.type
         interval = interval or self.interval
 
-        timeseries = obspy.core.Stream()
+        timeseries = Stream()
         urlIntervals = Util.get_intervals(
             starttime=starttime, endtime=endtime, size=self.urlInterval
         )
@@ -140,7 +177,7 @@ class TimeseriesFactory(object):
                 print("Error parsing data: " + str(e), file=sys.stderr)
                 print(data, file=sys.stderr)
         if channels is not None:
-            filtered = obspy.core.Stream()
+            filtered = Stream()
             for channel in channels:
                 filtered += timeseries.select(channel=channel)
             timeseries = filtered
@@ -152,6 +189,8 @@ class TimeseriesFactory(object):
             pad=True,
             fill_value=numpy.nan,
         )
+        if derive_missing:
+            timeseries = self.get_derived_timeseries(timeseries=timeseries)
         return timeseries
 
     def parse_string(self, data, **kwargs):
diff --git a/geomagio/TimeseriesUtility.py b/geomagio/TimeseriesUtility.py
index e0729c21b81796707a06cff1184765aeab5be29c..a2e152b682384b9fc1b71abf82ba64cd25fed442 100644
--- a/geomagio/TimeseriesUtility.py
+++ b/geomagio/TimeseriesUtility.py
@@ -3,6 +3,8 @@ from builtins import range
 from datetime import datetime
 import math
 import sys
+from typing import List
+
 import numpy
 from obspy.core import Stats, Stream, Trace, UTCDateTime
 
@@ -288,21 +290,22 @@ def get_merged_gaps(gaps):
     return merged_gaps
 
 
-def get_channels(stream):
+def get_channels(stream: Stream, contains_data: bool = False) -> List[str]:
     """Get a list of channels in a stream.
-
-    Parameters
-    ----------
-    stream : Stream
-
-    Returns
-    -------
-    channels : array_like
+    If contains_data is True, only channels containing data are returned.
     """
     channels = {}
     for trace in stream:
-        channel = trace.stats.channel
+        stats = trace.stats
+        channel = stats.channel
         if channel:
+            if contains_data and not has_all_channels(
+                stream=stream,
+                channels=(channel,),
+                starttime=stats.starttime,
+                endtime=stats.endtime,
+            ):
+                continue
             channels[channel] = True
     return [ch for ch in channels]
 
@@ -610,6 +613,9 @@ def split_trace(trace: Trace, size: int = 86400) -> Stream:
         size=size,
         trim=True,
     ):
+        if interval["start"] == interval["end"]:
+            stream += out_trace
+            continue
         stream += out_trace.slice(
             starttime=interval["start"],
             endtime=interval["end"] - out_trace.stats.delta,
diff --git a/geomagio/adjusted/AdjustedMatrix.py b/geomagio/adjusted/AdjustedMatrix.py
index 88e7169bff4200f65eb1899cfa89077afbafab5b..f02671ae2161d2e751b758b13cead236f97e73e1 100644
--- a/geomagio/adjusted/AdjustedMatrix.py
+++ b/geomagio/adjusted/AdjustedMatrix.py
@@ -32,7 +32,7 @@ class AdjustedMatrix(BaseModel):
     def process(
         self,
         stream: Stream,
-        inchannels=["H", "E", "Z", "F"],
+        inchannels=["U", "V", "W", "F"],
         outchannels=["X", "Y", "Z", "F"],
     ):
         """ Apply matrix to raw data. Apply pier correction to F when necessary """
diff --git a/geomagio/algorithm/AdjustedAlgorithm.py b/geomagio/algorithm/AdjustedAlgorithm.py
index fcbe2dcebb8e66e2d95e9235a7134e905f52eca6..ef92e2ed471e8b7e767f28b0a23a6377961b2065 100644
--- a/geomagio/algorithm/AdjustedAlgorithm.py
+++ b/geomagio/algorithm/AdjustedAlgorithm.py
@@ -23,7 +23,7 @@ class AdjustedAlgorithm(Algorithm):
         inchannels=None,
         outchannels=None,
     ):
-        inchannels = inchannels or ["H", "E", "Z", "F"]
+        inchannels = inchannels or ["U", "V", "W", "F"]
         outchannels = outchannels or ["X", "Y", "Z", "F"]
         Algorithm.__init__(
             self,
diff --git a/geomagio/algorithm/DeltaFAlgorithm.py b/geomagio/algorithm/DeltaFAlgorithm.py
index 22ebab00988a260a6fc986048ef076caa5b2ad2a..d2e1735f28f1b9a02c959864016f5685c94d0707 100644
--- a/geomagio/algorithm/DeltaFAlgorithm.py
+++ b/geomagio/algorithm/DeltaFAlgorithm.py
@@ -10,11 +10,11 @@ from .. import StreamConverter
 # List of channels by geomagnetic observatory orientation.
 # geo represents a geographic north/south orientation
 # obs represents the sensor orientation aligned close to the mag orientation
-# obsd is the same as obs,  but with D(declination) instead of E (e/w vector)
+# obsd is the same as obs,  but with D(declination) instead of V (e/w vector)
 CHANNELS = {
     "geo": ["X", "Y", "Z", "F"],
-    "obs": ["H", "E", "Z", "F"],
-    "obsd": ["H", "D", "Z", "F"],
+    "obs": ["U", "V", "W", "F"],
+    "obsd": ["U", "D", "W", "F"],
 }
 
 
diff --git a/geomagio/algorithm/FilterAlgorithm.py b/geomagio/algorithm/FilterAlgorithm.py
index da0f2df18f115f7be5d9e3d25c86147917a65e4a..304992771bd8eaeaa64abfcbb008dbb9d5549d75 100644
--- a/geomagio/algorithm/FilterAlgorithm.py
+++ b/geomagio/algorithm/FilterAlgorithm.py
@@ -411,7 +411,7 @@ class FilterAlgorithm(Algorithm):
         steps = self.get_filter_steps()
         # calculate start/end from inverted step array
         for step in reversed(steps):
-            start_interval = get_nearest_time(step=step, output_time=start, left=False)
+            start_interval = get_nearest_time(step=step, output_time=start, left=True)
             end_interval = get_nearest_time(step=step, output_time=end, left=True)
             start, end = start_interval["data_start"], end_interval["data_end"]
         return (start, end)
diff --git a/geomagio/algorithm/XYZAlgorithm.py b/geomagio/algorithm/XYZAlgorithm.py
index 1f43c87853c8da21d7a4636ad1bf7560f35e5471..766545224a6152b2c1c7a3e228560a1b97da9ef3 100644
--- a/geomagio/algorithm/XYZAlgorithm.py
+++ b/geomagio/algorithm/XYZAlgorithm.py
@@ -12,12 +12,12 @@ from .. import StreamConverter
 # geo represents a geographic north/south orientation
 # mag represents the (calculated)instantaneous mangnetic north orientation
 # obs represents the sensor orientation aligned close to the mag orientation
-# obsd is the same as obs,  but with D(declination) instead of E (e/w vector)
+# obsd is the same as obs,  but with D(declination) instead of V (e/w vector)
 CHANNELS = {
     "geo": ["X", "Y", "Z", "F"],
     "mag": ["H", "D", "Z", "F"],
-    "obs": ["H", "E", "Z", "F"],
-    "obsd": ["H", "D", "Z", "F"],
+    "obs": ["U", "V", "W", "F"],
+    "obsd": ["U", "D", "W", "F"],
 }
 
 
diff --git a/geomagio/derived.py b/geomagio/derived.py
new file mode 100644
index 0000000000000000000000000000000000000000..2f0807ecae2fe5d75d8270388d7d19bdb255654a
--- /dev/null
+++ b/geomagio/derived.py
@@ -0,0 +1,98 @@
+from typing import List
+
+from obspy import Stream, Trace
+
+from .algorithm.DeltaFAlgorithm import DeltaFAlgorithm
+from .algorithm.XYZAlgorithm import XYZAlgorithm
+from .TimeseriesUtility import get_channels, has_all_channels
+
+
+def compute_trace(timeseries: Stream, channel: str) -> Trace:
+    """calculates derived product from input timeseries"""
+    data_type = timeseries[0].stats.data_type
+    input_channels = get_derived_input_channels(channel=channel, data_type=data_type)
+    input_timeseries = Stream([timeseries.select(channel=c)[0] for c in input_channels])
+    input_format = _get_input_format(data_type=data_type)
+    output_format = _get_output_format(data_type=data_type, channel=channel)
+    if channel == "G":
+        return DeltaFAlgorithm(informat=input_format).process(
+            timeseries=input_timeseries
+        )[0]
+    elif channel in ["D", "X", "Y"] and data_type == "variation":
+        output_timeseries = XYZAlgorithm(
+            informat=input_format, outformat=output_format
+        ).process(timeseries=input_timeseries)
+    elif channel in ["D", "H"] and data_type == "adjusted":
+        output_timeseries = XYZAlgorithm(
+            informat=input_format, outformat=output_format
+        ).process(timeseries=input_timeseries)
+    else:
+        output_timeseries = input_timeseries.select(channel=channel)
+    return output_timeseries.select(channel=channel)[0]
+
+
+def get_timeseries(timeseries: Stream, channels: List[str]) -> Stream:
+    """calculates requested derived products from input timeseries"""
+    output_timeseries = Stream()
+    for channel in channels:
+        output_timeseries += compute_trace(timeseries=timeseries, channel=channel)
+    return output_timeseries
+
+
+def get_missing_channels(timeseries: Stream) -> List[str]:
+    """retrieves empty channels from stream"""
+    missing_channels = []
+    stats = timeseries[0].stats
+    for channel in get_channels(stream=timeseries):
+        if not has_all_channels(
+            stream=timeseries,
+            channels=(channel,),
+            starttime=stats.starttime,
+            endtime=stats.endtime,
+        ):
+            missing_channels.append(channel)
+    return missing_channels
+
+
+def get_derived_input_channels(channel: str, data_type: str) -> List[str]:
+    """returns channels needed to compute derived channel"""
+    if channel == "G":
+        if data_type == "variation":
+            return ["U", "V", "W", "F"]
+        else:
+            return ["X", "Y", "Z", "F"]
+    elif channel in ["X", "Y", "H", "D"] and data_type == "variation":
+        return ["U", "V"]
+    elif channel in ["H", "D"] and data_type != "variation":
+        return ["X", "Y"]
+    else:
+        # not a derived channel
+        return [channel]
+
+
+def get_required_channels(channels: List[str], data_type: str) -> List[str]:
+    """returns channels needed to compute derived channels"""
+    required_channels = []
+    for channel in channels:
+        input_channels = get_derived_input_channels(
+            channel=channel, data_type=data_type
+        )
+        required_channels.extend(
+            [c for c in input_channels if c not in required_channels]
+        )
+    return required_channels
+
+
+def _get_input_format(data_type: str) -> str:
+    if data_type == "variation":
+        return "obs"
+    return "geo"
+
+
+def _get_output_format(data_type: str, channel: str) -> str:
+    if data_type == "variation":
+        if channel in ["X", "Y"]:
+            return "geo"
+        elif channel == "D":
+            return "obsd"
+    return "mag"
diff --git a/geomagio/edge/EdgeFactory.py b/geomagio/edge/EdgeFactory.py
index 682d03f733ed2b24fa04269925859f3279f179ed..90a68111306d3eb14faa374d39ad17c49206ea2b 100644
--- a/geomagio/edge/EdgeFactory.py
+++ b/geomagio/edge/EdgeFactory.py
@@ -111,6 +111,7 @@ class EdgeFactory(TimeseriesFactory):
         channels=None,
         type=None,
         interval=None,
+        derive_missing: bool = True,
     ):
         """Get timeseries data
 
@@ -128,6 +129,8 @@ class EdgeFactory(TimeseriesFactory):
             data type.
         interval: {'day', 'hour', 'minute', 'second', 'tenhertz'}
             data interval.
+        dervie_missing
+            if True, calculates trivial derived products missing from initial request
 
         Returns
         -------
@@ -166,7 +169,8 @@ class EdgeFactory(TimeseriesFactory):
             # restore stdout
             sys.stdout = original_stdout
         self._post_process(timeseries, starttime, endtime, channels)
-
+        if derive_missing:
+            timeseries = self.get_derived_timeseries(timeseries=timeseries)
         return timeseries
 
     def put_timeseries(
@@ -200,6 +204,11 @@ class EdgeFactory(TimeseriesFactory):
             channel and that trace should have an ndarray, with nan's
             representing gaps.
         """
+        if not TimeseriesUtility.has_any_channels(
+            stream=timeseries, channels=channels, starttime=starttime, endtime=endtime
+        ):
+            sys.stderr.write("No data to write\n")
+            return
         stats = timeseries[0].stats
         observatory = observatory or stats.station or self.observatory
         channels = channels or self.channels
@@ -443,6 +452,7 @@ class EdgeFactory(TimeseriesFactory):
                     trace_send.data
                 )
             trace_send = self._convert_trace_to_int(trace_send)
+            trace.stats.channel = sncl.element
             ric.send_trace(interval, trace_send)
         if self.forceout:
             ric.forceout()
diff --git a/geomagio/edge/LegacySNCL.py b/geomagio/edge/LegacySNCL.py
index 1b8cd2169c2ac5b4e45c207bd116e343f2032f44..87f47d5f8be9f178ccc5a22e7f68712100260bda 100644
--- a/geomagio/edge/LegacySNCL.py
+++ b/geomagio/edge/LegacySNCL.py
@@ -31,7 +31,9 @@ class LegacySNCL(SNCL):
         return LegacySNCL(
             station=station,
             network=network,
-            channel=get_channel(element=element, interval=interval),
+            channel=get_channel(
+                data_type=data_type, element=element, interval=interval
+            ),
             location=location or get_location(element=element, data_type=data_type),
         )
 
@@ -55,9 +57,10 @@ class LegacySNCL(SNCL):
         raise ValueError(f"Unexcepted interval code: {channel_start}")
 
 
-def get_channel(element: str, interval: str) -> str:
+def get_channel(data_type: str, element: str, interval: str) -> str:
     return _check_predefined_channel(element=element, interval=interval) or (
-        _get_channel_start(interval=interval) + _get_channel_end(element=element)
+        _get_channel_start(interval=interval)
+        + _get_channel_end(data_type=data_type, element=element)
     )
 
 
@@ -87,6 +90,12 @@ def _get_channel_start(interval: str) -> str:
 def _get_element(channel: str, location: str) -> str:
     """Translates channel/location to element"""
     element_start = channel[2]
+    if element_start == "U":
+        element_start = "H"
+    elif element_start == "V":
+        element_start = "E"
+    elif element_start == "W":
+        element_start = "Z"
     channel = channel
     channel_middle = channel[1]
     location_end = location[1]
@@ -116,7 +125,7 @@ def _check_predefined_channel(element: str, interval: str) -> Optional[str]:
         return None
 
 
-def _get_channel_end(element: str) -> str:
+def _get_channel_end(data_type: str, element: str) -> str:
     channel_middle = "V"
     if "_Volt" in element:
         channel_middle = "E"
@@ -127,6 +136,12 @@ def _get_channel_end(element: str) -> str:
     elif element in ["F", "G"]:
         channel_middle = "S"
     channel_end = element.split("_")[0]
+    if channel_end == "U" and data_type == "variation":
+        channel_end = "H"
+    elif channel_end == "V" and data_type == "variation":
+        channel_end = "E"
+    elif channel_end == "W" and data_type == "variation":
+        channel_end = "Z"
     return channel_middle + channel_end
 
 
diff --git a/geomagio/edge/MiniSeedFactory.py b/geomagio/edge/MiniSeedFactory.py
index d56bab7713c0684c199fa227a8d815d3621c9cb1..d94437114f6cd716ce123c94ecf3dac2d75abfd8 100644
--- a/geomagio/edge/MiniSeedFactory.py
+++ b/geomagio/edge/MiniSeedFactory.py
@@ -68,7 +68,7 @@ class MiniSeedFactory(TimeseriesFactory):
         self,
         host="cwbpub.cr.usgs.gov",
         port=2061,
-        write_port=7981,
+        write_port=7974,
         observatory=None,
         channels=None,
         type=None,
@@ -97,6 +97,7 @@ class MiniSeedFactory(TimeseriesFactory):
         channels=None,
         type=None,
         interval=None,
+        derive_missing: bool = True,
     ):
         """Get timeseries data
 
@@ -114,6 +115,8 @@ class MiniSeedFactory(TimeseriesFactory):
             data type.
         interval: {'day', 'hour', 'minute', 'second', 'tenhertz'}
             data interval.
+        derive_missing
+            if True, calculates trivial derived products missing from initial request
 
         Returns
         -------
@@ -158,6 +161,8 @@ class MiniSeedFactory(TimeseriesFactory):
             sys.stdout = original_stdout
 
         self._post_process(timeseries, starttime, endtime, channels)
+        if derive_missing:
+            self.get_derived_timeseries(timeseries=timeseries)
         return timeseries
 
     def put_timeseries(
@@ -191,6 +196,11 @@ class MiniSeedFactory(TimeseriesFactory):
             channel and that trace should have an ndarray, with nan's
             representing gaps.
         """
+        if not TimeseriesUtility.has_any_channels(
+            stream=timeseries, channels=channels, starttime=starttime, endtime=endtime
+        ):
+            sys.stderr.write("No data to write\n")
+            return
         stats = timeseries[0].stats
         observatory = observatory or stats.station or self.observatory
         channels = channels or self.channels
@@ -329,6 +339,10 @@ class MiniSeedFactory(TimeseriesFactory):
         data = self.client.get_waveforms(
             sncl.network, sncl.station, sncl.location, sncl.channel, starttime, endtime
         )
+        if len(data) > 1:
+            data = obspy.core.Stream(
+                [d for d in data if d.data.dtype != "int32"]
+            )  # for shared channels names between earthworm and miniseed(T1-4)
         data.merge()
         if data.count() == 0:
             data += TimeseriesUtility.create_empty_trace(
diff --git a/geomagio/edge/MiniSeedInputClient.py b/geomagio/edge/MiniSeedInputClient.py
index a25d31ce89741d8483aa9a1bf55220b1d2c3358e..d2dc2e148e6bb8351b846bc2ccca4d268a544a35 100644
--- a/geomagio/edge/MiniSeedInputClient.py
+++ b/geomagio/edge/MiniSeedInputClient.py
@@ -25,7 +25,7 @@ class MiniSeedInputClient(object):
         Floating point precision for output data
     """
 
-    def __init__(self, host, port=2061, encoding="float32"):
+    def __init__(self, host, port=2061, encoding="float64"):
         self.host = host
         self.port = port
         self.encoding = encoding
diff --git a/geomagio/processing/__init__.py b/geomagio/processing/__init__.py
index be97cfbd5e32485df54fc814fd646c595685fed7..a429346ca2b1f86fe0aa54564cf1c56321be0a1d 100644
--- a/geomagio/processing/__init__.py
+++ b/geomagio/processing/__init__.py
@@ -3,21 +3,26 @@
 Note that these implementations are subject to change,
 and should be considered less stable than other packages in the library.
 """
+from .derived import adjusted, average, sqdist
 from .factory import get_edge_factory, get_miniseed_factory
-from .observatory import adjusted, average, deltaf, rotate, sqdist_minute
-from .obsrio import obsrio_minute, obsrio_second, obsrio_temperatures, obsrio_tenhertz
+from .obsrio import (
+    filter_days,
+    filter_hours,
+    filter_minutes,
+    filter_temperatures,
+    filter_tenhertz,
+)
 
 
 __all__ = [
     "adjusted",
     "average",
-    "deltaf",
+    "filter_days",
+    "filter_hours",
+    "filter_minutes",
+    "filter_temperatures",
+    "filter_tenhertz",
     "get_edge_factory",
     "get_miniseed_factory",
-    "obsrio_minute",
-    "obsrio_second",
-    "obsrio_temperatures",
-    "obsrio_tenhertz",
-    "rotate",
-    "sqdist_minute",
+    "sqdist",
 ]
diff --git a/geomagio/processing/derived.py b/geomagio/processing/derived.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8f4a565fc11f902313bd4dce139d5082f32064f
--- /dev/null
+++ b/geomagio/processing/derived.py
@@ -0,0 +1,108 @@
+from typing import Optional, List
+
+from obspy import UTCDateTime
+
+from ..algorithm import (
+    AdjustedAlgorithm,
+    AverageAlgorithm,
+    SqDistAlgorithm,
+)
+from ..Controller import Controller, get_realtime_interval
+from ..edge import MiniSeedFactory
+from .factory import get_miniseed_factory
+
+
+def adjusted(
+    observatory: str,
+    statefile: str,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    update_limit: int = 10,
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
+):
+    """ Transform 1Hz UVWF to 1HZ XYZF """
+    controller = Controller(
+        inputFactory=input_factory or get_miniseed_factory(data_type="variation"),
+        outputFactory=output_factory or get_miniseed_factory(data_type="adjusted"),
+        algorithm=AdjustedAlgorithm(
+            statefile=statefile,
+            data_type="adjusted",
+            location="A0",
+        ),
+        inputInterval="second",
+        outputInterval="second",
+    )
+    controller.run_as_update(
+        observatory=(observatory,),
+        output_observatory=(observatory,),
+        starttime=starttime,
+        endtime=endtime,
+        input_channels=("U", "V", "W", "F"),
+        output_channels=("X", "Y", "Z", "F"),
+        realtime=endtime - starttime,
+        update_limit=update_limit,
+    )
+
+
+def average(
+    observatories: List[str],
+    input_channel: str,
+    output_channel: str,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    update_limit: int = 10,
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
+):
+    """ Computes single channel average across multiple observatories """
+    controller = Controller(
+        inputFactory=input_factory,
+        outputFactory=output_factory,
+        algorithm=AverageAlgorithm(observatories=observatories, channel=output_channel),
+        inputInterval="minute",
+        outputInterval="minute",
+    )
+    controller.run_as_update(
+        observatory=observatories,
+        output_observatory="USGS",
+        starttime=starttime,
+        endtime=endtime,
+        input_channels=(input_channel,),
+        output_channels=(output_channel,),
+        realtime=endtime - starttime,
+        update_limit=update_limit,
+    )
+
+
+def sqdist(
+    observatory: str,
+    statefile: str,
+    realtime_interval: int = 1800,
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
+):
+    """ Computes H_SQ, H_SV, and H_Dist """
+    starttime, endtime = get_realtime_interval(interval_seconds=realtime_interval)
+    controller = Controller(
+        inputFactory=input_factory or get_miniseed_factory(interval="minute"),
+        outputFactory=output_factory or get_miniseed_factory(interval="minute"),
+        algorithm=SqDistAlgorithm(
+            alpha=2.3148e-5,
+            gamma=3.3333e-2,
+            beta=0,
+            m=1440,
+            mag=True,
+            smooth=180,
+            statefile=statefile,
+        ),
+        inputInterval="minute",
+        outputInterval="minute",
+    )
+    controller.run(
+        observatory=(observatory,),
+        starttime=starttime,
+        endtime=endtime,
+        input_channels=("X", "Y", "Z", "F"),
+        output_channels=("H_Dist", "H_SQ", "H_SV"),
+    )
diff --git a/geomagio/processing/factory.py b/geomagio/processing/factory.py
index d9a065497d9cf6b0a38c391355e11a9d698795d2..a12bd93e6c5ceab6df5dc5bc11c3316d08383ddd 100644
--- a/geomagio/processing/factory.py
+++ b/geomagio/processing/factory.py
@@ -1,27 +1,31 @@
 import os
 
-from ..TimeseriesFactory import TimeseriesFactory
 from ..edge import EdgeFactory, MiniSeedFactory
 
 
 def get_edge_factory(
-    data_type="variation", interval="second", **kwargs
-) -> TimeseriesFactory:
+    host=os.getenv("DATA_HOST", "127.0.0.1"),
+    data_type="variation",
+    interval="second",
+    **kwargs,
+):
     return EdgeFactory(
-        host=os.getenv("EDGE_HOST", "127.0.0.1"),
-        interval=interval,
+        host=host,
         type=data_type,
-        **kwargs
+        interval=interval,
+        **kwargs,
     )
 
 
 def get_miniseed_factory(
-    data_type="variation", interval="second", **kwargs
-) -> TimeseriesFactory:
+    host=os.getenv("DATA_HOST", "127.0.0.1"),
+    data_type="variation",
+    interval="second",
+    **kwargs,
+):
     return MiniSeedFactory(
-        convert_channels=("U", "V", "W"),
-        host=os.getenv("EDGE_HOST", "127.0.0.1"),
-        interval=interval,
+        host=host,
         type=data_type,
-        **kwargs
+        interval=interval,
+        **kwargs,
     )
diff --git a/geomagio/processing/observatory.py b/geomagio/processing/observatory.py
index 85a9cfd7f033c4e72bb3401456f939c4e154a8c1..d31b31517c468d4d4c8e492f35de16c14cd43af1 100644
--- a/geomagio/processing/observatory.py
+++ b/geomagio/processing/observatory.py
@@ -1,243 +1,372 @@
-from typing import List, Optional
+from enum import Enum
+import json
+from typing import List, Optional, Tuple
 
-import numpy
+from obspy import UTCDateTime
+import typer
 
-from ..algorithm import (
-    AdjustedAlgorithm,
-    AverageAlgorithm,
-    DeltaFAlgorithm,
-    SqDistAlgorithm,
-    XYZAlgorithm,
-)
-from ..Controller import Controller, get_realtime_interval
-from ..TimeseriesFactory import TimeseriesFactory
-from .factory import get_edge_factory
+from ..Controller import get_realtime_interval
+from .factory import get_edge_factory, get_miniseed_factory
+from . import obsrio, derived, pcdcp
 
 
-def adjusted(
-    observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    interval: str = "second",
-    output_factory: Optional[TimeseriesFactory] = None,
-    matrix: Optional[numpy.ndarray] = None,
-    pier_correction: Optional[float] = None,
-    statefile: Optional[str] = None,
+app = typer.Typer()
+
+
+def main():
+    app()
+
+
+class DataFormat(str, Enum):
+    OBSRIO = "obsrio"
+    PCDCP = "pcdcp"
+
+
+class DataType(str, Enum):
+    ADJUSTED = "adjusted"
+    VARIATION = "variation"
+
+
+@app.command(
+    name="average",
+    short_help="computes Dst-3 and Dst-4 from 1-minute variation data",
+    help="""
+    Dst-3: Computed from HON, GUA and SJG(H_Dist)
+
+    Dst-4: Computed from HON, SJG, HER, KAK(H_Dist)
+    """,
+)
+def compute_average(
+    data_type: DataType = DataType.VARIATION,
     realtime_interval: int = 600,
+    update_limit: int = 10,
+    input_host: str = "127.0.0.1",
+    output_host: str = "127.0.0.1",
+    starttime: Optional[str] = None,
+    endtime: Optional[str] = None,
 ):
-    """Run Adjusted algorithm.
-
-    Parameters
-    ----------
-    observatory: observatory to calculate
-    input_factory: where to read, should be configured with data_type
-    interval: data interval
-    output_factory: where to write, should be configured with data_type
-    matrix: adjusted matrix
-    pier_correction: adjusted pier correction
-    statefile: adjusted statefile
-    realtime_interval: window in seconds
-
-    Uses update_limit=10.
-    """
-    if not statefile and (not matrix or not pier_correction):
-        raise ValueError("Either statefile or matrix and pier_correction are required.")
-    starttime, endtime = get_realtime_interval(realtime_interval)
-    controller = Controller(
-        algorithm=AdjustedAlgorithm(
-            matrix=matrix,
-            pier_correction=pier_correction,
-            statefile=statefile,
-            data_type="adjusted",
-            location="A0",
-        ),
-        inputFactory=input_factory or get_edge_factory(data_type="variation"),
-        inputInterval=interval,
-        outputFactory=output_factory or get_edge_factory(data_type="adjusted"),
-        outputInterval=interval,
+    starttime, endtime = get_processing_interval(
+        realtime_interval=realtime_interval,
+        starttime=starttime,
+        endtime=endtime,
+    )
+    input_factory = get_miniseed_factory(host=input_host, data_type=data_type)
+    output_factory = get_miniseed_factory(host=output_host, data_type=data_type)
+    derived.average(
+        observatories=["HON", "SJG", "HER", "KAK"],
+        input_channel="H_Dist",
+        output_channel="UX4",
+        starttime=starttime,
+        endtime=endtime,
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
     )
-    controller.run_as_update(
-        observatory=(observatory,),
-        output_observatory=(observatory,),
+    derived.average(
+        observatories=["HON", "GUA", "SJG"],
+        input_channel="H_Dist",
+        output_channel="UX3",
         starttime=starttime,
         endtime=endtime,
-        input_channels=("H", "E", "Z", "F"),
-        output_channels=("X", "Y", "Z", "F"),
-        realtime=realtime_interval,
-        update_limit=10,
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
     )
 
 
-def average(
-    observatories: List[str],
-    input_channel: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    interval: str = "second",
-    output_channel: str = None,
-    output_factory: Optional[TimeseriesFactory] = None,
-    output_observatory: str = "USGS",
+@app.command(
+    name="adjusted",
+    short_help="Computes adjusted XYZF and derived channels at 1Hz and 1 minute",
+    help="""
+    Inputs: U, V, W, F(1Hz)
+
+    Outputs: X, Y, Z, F, H_Dist, H_SQ, H_SV(1Hz/1 min)
+    """,
+)
+def process_adjusted(
+    observatory: str,
+    adjusted_statefile: str,
+    sqdist_statefile: str,
     realtime_interval: int = 600,
+    update_limit: int = 10,
+    input_host: str = "127.0.0.1",
+    output_host: str = "127.0.0.1",
+    starttime: Optional[str] = None,
+    endtime: Optional[str] = None,
 ):
-    """Run Average algorithm.
-
-    Parameters
-    ----------
-    observatories: input observatories to calculate
-    input_channel: channel from multiple observatories to average
-    input_factory: where to read, should be configured with data_type and interval
-    interval: data interval
-    output_channel: channel to write (defaults to input_channel).
-    output_factory: where to write, should be configured with data_type and interval
-    output_observatory: observatory where output is written
-    realtime_interval: window in seconds
-
-    Uses update_limit=10.
-    """
-    starttime, endtime = get_realtime_interval(realtime_interval)
-    controller = Controller(
-        algorithm=AverageAlgorithm(observatories=observatories, channel=output_channel),
-        inputFactory=input_factory or get_edge_factory(),
-        inputInterval=interval,
-        outputFactory=output_factory or get_edge_factory(),
-        outputInterval=interval,
+    process_sqdist = False
+    if starttime is None and endtime is None:
+        process_sqdist = True
+    starttime, endtime = get_processing_interval(
+        realtime_interval=realtime_interval,
+        starttime=starttime,
+        endtime=endtime,
+    )
+    input_factory = get_miniseed_factory(host=input_host, data_type="adjusted")
+    output_factory = get_miniseed_factory(host=output_host, data_type="adjusted")
+    derived.adjusted(
+        observatory=observatory,
+        statefile=adjusted_statefile,
+        starttime=starttime,
+        endtime=endtime,
+        update_limit=update_limit,
+        input_factory=get_miniseed_factory(host=input_host, data_type="variation"),
+        output_factory=output_factory,
     )
-    controller.run_as_update(
-        observatory=observatories,
-        output_observatory=(output_observatory,),
+    obsrio.filter_minutes(
+        observatory=observatory,
         starttime=starttime,
         endtime=endtime,
-        output_channels=(output_channel or input_channel,),
-        realtime=realtime_interval,
-        update_limit=10,
+        update_limit=update_limit,
+        channels=("X", "Y", "Z", "F"),
+        input_factory=input_factory,
+        output_factory=output_factory,
     )
+    if process_sqdist and valid_sqdist_statefile(
+        statefile=sqdist_statefile, starttime=starttime
+    ):
+        derived.sqdist(
+            observatory=observatory,
+            statefile=sqdist_statefile,
+            realtime_interval=realtime_interval,
+            input_factory=input_factory,
+            output_factory=output_factory,
+        )
+    pcdcp.legacy_adjusted(
+        observatory=observatory,
+        starttime=starttime,
+        endtime=endtime,
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=get_edge_factory(host=output_host, data_type="adjusted"),
+    )
+
+
+@app.command(
+    name="day",
+    short_help="Computes daily UVWF and derived channels(variation/adjusted)",
+    help="""
+    Variation:
+
+        Inputs: U, V, W, F(1 min)
+
+        Outputs: U, V, W, F(1 day)
+
+    Adjusted:
 
+        Inputs: X, Y, Z, F(1 min)
 
-def deltaf(
+        Outputs: X, Y, Z, F(1 day)
+    """,
+)
+def process_day(
     observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    interval: str = "second",
-    output_factory: Optional[TimeseriesFactory] = None,
-    deltaf_from="obs",
-    realtime_interval: int = 600,
+    data_type: DataType = DataType.VARIATION,
+    realtime_interval: int = 86400,
+    update_limit: int = 7,
+    input_host: str = "127.0.0.1",
+    output_host: str = "127.0.0.1",
+    starttime: Optional[str] = None,
+    endtime: Optional[str] = None,
 ):
-    """Run Delta-F algorithm.
-
-    Parameters
-    ----------
-    observatory: observatory to calculate
-    input_factory: where to read, should be configured with data_type and interval
-    output_factory: where to write, should be configured with data_type and interval
-    deltaf_from: one of {"obs", "mag", "geo"}
-    realtime_interval: window in seconds
-
-    Uses update_limit=10.
-    """
-    starttime, endtime = get_realtime_interval(realtime_interval)
-    controller = Controller(
-        algorithm=DeltaFAlgorithm(informat=deltaf_from),
-        inputFactory=input_factory or get_edge_factory(),
-        inputInterval=interval,
-        outputFactory=output_factory or get_edge_factory(),
-        outputInterval=interval,
+    starttime, endtime = get_processing_interval(
+        realtime_interval=realtime_interval,
+        starttime=starttime,
+        endtime=endtime,
     )
-    controller.run_as_update(
-        observatory=(observatory,),
-        output_observatory=(observatory,),
+    input_factory = get_miniseed_factory(host=input_host, data_type=data_type)
+    output_factory = get_miniseed_factory(host=output_host, data_type=data_type)
+    obsrio.filter_days(
+        observatory=observatory,
         starttime=starttime,
         endtime=endtime,
-        output_channels=("G",),
-        realtime=realtime_interval,
-        update_limit=10,
+        channels=("U", "V", "W", "F")
+        if data_type == DataType.VARIATION
+        else ("X", "Y", "Z", "F"),
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
     )
 
 
-def rotate(
+@app.command(
+    name="hour",
+    short_help="Computes hourly UVWF and derived channels(variation/adjusted)",
+    help="""
+    Variation:
+
+        Inputs: U, V, W, F(1 min)
+
+        Outputs: U, V, W, F(1 hour)
+
+    Adjusted:
+
+        Inputs: X, Y, Z, F(1 min)
+
+        Outputs: X, Y, Z, F(1 hour)
+    """,
+)
+def process_hour(
     observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    interval: str = "second",
-    output_channels=("X", "Y"),
-    output_factory: Optional[TimeseriesFactory] = None,
-    realtime_interval: int = 600,
-    xyz_from="obs",
-    xyz_to="geo",
+    data_type: DataType = DataType.VARIATION,
+    realtime_interval: int = 3600,
+    update_limit: int = 7,
+    input_host: str = "127.0.0.1",
+    output_host: str = "127.0.0.1",
+    starttime: Optional[str] = None,
+    endtime: Optional[str] = None,
 ):
-    """Run XYZ rotation algorithm.
-
-    Parameters
-    ----------
-    observatory: observatory to calculate
-    input_factory: where to read, should be configured with data_type and interval
-    output_channels: which channels to write
-    output_factory: where to write, should be configured with data_type and interval
-    realtime_interval: window in seconds
-    xyz_from: one of {"obs", "mag", "geo"}
-    xyz_to: one of {"obs", "obsd", "mag", "geo"}
-
-    Uses update_limit=10.
-    """
-    starttime, endtime = get_realtime_interval(realtime_interval)
-    controller = Controller(
-        algorithm=XYZAlgorithm(informat=xyz_from, outformat=xyz_to),
-        inputFactory=input_factory or get_edge_factory(),
-        inputInterval=interval,
-        outputFactory=output_factory or get_edge_factory(),
-        outputInterval=interval,
+    starttime, endtime = get_processing_interval(
+        realtime_interval=realtime_interval,
+        starttime=starttime,
+        endtime=endtime,
     )
-    controller.run_as_update(
-        observatory=(observatory,),
-        output_observatory=(observatory,),
+    input_factory = get_miniseed_factory(host=input_host, data_type=data_type)
+    output_factory = get_miniseed_factory(host=output_host, data_type=data_type)
+    obsrio.filter_hours(
+        observatory=observatory,
         starttime=starttime,
         endtime=endtime,
-        output_channels=output_channels,
-        realtime=realtime_interval,
-        update_limit=10,
+        channels=("U", "V", "W", "F")
+        if data_type == DataType.VARIATION
+        else ("X", "Y", "Z", "F"),
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
     )
 
 
-def sqdist_minute(
+@app.command(
+    name="variation",
+    short_help="Computes variation UVWF and derived channels at 1Hz and 1 minute",
+    help="""
+    Inputs:
+
+        PCDCP:
+
+            H, E, Z, F(1Hz)
+
+            T1-4(1 min)
+
+        OBSRIO:
+
+            U, V ,W(10Hz)
+
+            F, T1-4(1Hz)
+
+    Outputs:
+
+        U, V, W, F(1 Hz)
+
+        U, V, W, F, T1-4, H_SQ, H_SV, H_Dist(1 min)
+    """,
+)
+def process_variation(
     observatory: str,
-    statefile: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    output_factory: Optional[TimeseriesFactory] = None,
-    realtime_interval: int = 1800,
+    sqdist_statefile: str,
+    data_format: DataFormat = DataFormat.PCDCP,
+    realtime_interval: int = 600,
+    update_limit: int = 10,
+    input_host: str = "127.0.0.1",
+    output_host: str = "127.0.0.1",
+    starttime: Optional[str] = None,
+    endtime: Optional[str] = None,
 ):
-    """Run SqDist algorithm.
-
-    Only supports "minute" interval.
-
-    Parameters
-    ----------
-    observatory: observatory to calculate
-    statefile: sqdist statefile must already exist
-    input_factory: where to read, should be configured with data_type and interval
-    output_factory: where to write, should be configured with data_type and interval
-    realtime_interval: window in seconds
-    """
-    if not statefile:
-        raise ValueError("Statefile is required.")
-    starttime, endtime = get_realtime_interval(realtime_interval)
-    controller = Controller(
-        algorithm=SqDistAlgorithm(
-            alpha=2.3148e-5,
-            gamma=3.3333e-2,
-            m=1440,
-            mag=True,
-            smooth=180,
-            statefile=statefile,
-        ),
-        inputFactory=input_factory or get_edge_factory(interval="minute"),
-        inputInterval="minute",
-        outputFactory=output_factory or get_edge_factory(interval="minute"),
-        outputInterval="minute",
+    process_sqdist = False
+    if starttime is None and endtime is None:
+        process_sqdist = True
+    starttime, endtime = get_processing_interval(
+        realtime_interval=realtime_interval,
+        starttime=starttime,
+        endtime=endtime,
+    )
+    input_factory = get_miniseed_factory(host=input_host)
+    output_factory = get_miniseed_factory(host=output_host)
+    if data_format == DataFormat.PCDCP:
+        pcdcp.prepare(
+            observatory=observatory,
+            starttime=starttime,
+            endtime=endtime,
+            update_limit=update_limit,
+            input_factory=get_edge_factory(host=input_host),
+            output_factory=output_factory,
+        )
+    else:
+        obsrio.prepare(
+            observatory=observatory,
+            starttime=starttime,
+            endtime=endtime,
+            update_limit=update_limit,
+            input_factory=get_miniseed_factory(
+                host=input_host, convert_channels=("U", "V", "W")
+            ),
+            output_factory=output_factory,
+        )
+        obsrio.copy_channels(
+            observatory=observatory,
+            channels=[
+                ("UK1", "UK1"),
+                ("UK2", "UK2"),
+                ("UK3", "UK3"),
+                ("UK4", "UK4"),
+            ],
+            starttime=starttime,
+            endtime=endtime,
+            data_interval="minute",
+            update_limit=update_limit,
+            input_factory=input_factory,
+            output_factory=get_edge_factory(host=input_host),
+        )
+    obsrio.filter_minutes(
+        observatory=observatory,
+        starttime=starttime,
+        endtime=endtime,
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
     )
-    # sqdist is stateful, use run
-    controller.run(
-        observatory=(observatory,),
-        output_observatory=(observatory,),
+    if process_sqdist and valid_sqdist_statefile(
+        statefile=sqdist_statefile, starttime=starttime
+    ):
+        derived.sqdist(
+            observatory=observatory,
+            statefile=sqdist_statefile,
+            realtime_interval=realtime_interval,
+            input_factory=input_factory,
+            output_factory=output_factory,
+        )
+    pcdcp.legacy_variation(
+        observatory=observatory,
         starttime=starttime,
         endtime=endtime,
-        input_channels=("X", "Y", "Z", "F"),
-        output_channels=("MDT", "MSQ", "MSV"),
-        realtime=realtime_interval,
-        rename_output_channel=(("H_Dist", "MDT"), ("H_SQ", "MSQ"), ("H_SV", "MSV")),
-        update_limit=10,
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=get_edge_factory(host=output_host),
     )
+
+
+def get_processing_interval(
+    realtime_interval: int,
+    starttime: Optional[str] = None,
+    endtime: Optional[str] = None,
+) -> Tuple[UTCDateTime, UTCDateTime]:
+    if starttime is not None and endtime is not None:
+        return (UTCDateTime(starttime), UTCDateTime(endtime))
+    elif starttime is not None and endtime is None:
+        raise ValueError("endtime not provided")
+    elif starttime is None and endtime is not None:
+        raise ValueError("starttime not provided")
+    else:
+        return get_realtime_interval(interval_seconds=realtime_interval)
+
+
+def valid_sqdist_statefile(statefile: str, starttime: UTCDateTime) -> bool:
+    try:
+        with open(statefile, "r") as f:
+            data = f.read()
+            data = json.loads(data)
+        if data["next_starttime"] > starttime:
+            return False
+        return True
+    except:
+        return False
diff --git a/geomagio/processing/obsrio.py b/geomagio/processing/obsrio.py
index bf0acf2b81687c6a3f590e6b4cd8b902f4730aa3..2db021e02b9b3ce52c90208ecba5f27d6369f94c 100644
--- a/geomagio/processing/obsrio.py
+++ b/geomagio/processing/obsrio.py
@@ -1,341 +1,262 @@
-from typing import Optional
+from typing import List, Optional, Tuple
 
-import typer
+from obspy import UTCDateTime
 
 from ..algorithm import Algorithm, FilterAlgorithm
-from ..edge import EdgeFactory, MiniSeedFactory
-from ..Controller import (
-    Controller,
-    get_realtime_interval,
-)
-from ..TimeseriesFactory import TimeseriesFactory
+from ..Controller import Controller
+from ..edge import MiniSeedFactory
+from .. import TimeseriesFactory
 from .factory import get_edge_factory, get_miniseed_factory
 
 
-def main():
-    typer.run(obsrio_filter)
-
-
-def obsrio_filter(
-    interval: str,
+def copy_channels(
     observatory: str,
-    input_factory: Optional[str] = None,
-    host: str = "127.0.0.1",
-    port: str = 2061,
-    output_factory: Optional[str] = None,
-    output_port: int = typer.Option(
-        2061, help="Port where output factory writes data."
-    ),
-    output_read_port: int = typer.Option(
-        2061, help="Port where output factory reads data"
-    ),
-    realtime_interval: int = 600,
+    channels: List[Tuple[str]],
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    data_interval: str = "second",
     update_limit: int = 10,
+    input_factory: Optional[TimeseriesFactory] = None,
+    output_factory: Optional[TimeseriesFactory] = None,
 ):
-    if interval == "realtime":
-        filter_realtime(
-            observatory=observatory,
-            input_factory=input_factory,
-            host=host,
-            port=port,
-            output_factory=output_factory,
-            output_port=output_port,
-            output_read_port=output_read_port,
-            realtime_interval=realtime_interval,
+    """ Copy channels between legacy and miniseed formats """
+    controller = Controller(
+        inputFactory=input_factory or get_miniseed_factory(),
+        outputFactory=output_factory or get_edge_factory(),
+        inputInterval=data_interval,
+        outputInterval=data_interval,
+    )
+    for input_channel, output_channel in channels:
+        controller.run_as_update(
+            algorithm=Algorithm(
+                inchannels=input_channel,
+                outchannels=output_channel,
+            ),
+            observatory=(observatory,),
+            output_observatory=(observatory,),
+            starttime=starttime,
+            endtime=endtime,
+            input_channels=(input_channel,),
+            output_channels=(output_channel,),
+            rename_output_channel=((input_channel, output_channel),),
+            realtime=endtime - starttime,
             update_limit=update_limit,
         )
-    elif interval in ["hour", "day"]:
-        input_factory = EdgeFactory(host=host, port=port)
-        output_factory = MiniSeedFactory(
-            host=host, port=output_read_port, write_port=output_port
-        )
-        if interval == "hour":
-            obsrio_hour(
-                observatory=observatory,
-                input_factory=input_factory,
-                output_factory=output_factory,
-                realtime_interval=realtime_interval,
-                update_limit=update_limit,
-            )
-        elif interval == "day":
-            obsrio_day(
-                observatory=observatory,
-                input_factory=input_factory,
-                output_factory=output_factory,
-                realtime_interval=realtime_interval,
-                update_limit=update_limit,
-            )
-    else:
-        raise ValueError("Invalid interval")
 
 
-def filter_realtime(
+def filter_days(
     observatory: str,
-    input_factory: Optional[str] = None,
-    host: str = "127.0.0.1",
-    port: str = 2061,
-    output_factory: Optional[str] = None,
-    output_port: int = typer.Option(
-        2061, help="Port where output factory writes data."
-    ),
-    output_read_port: int = typer.Option(
-        2061, help="Port where output factory reads data"
-    ),
-    realtime_interval: int = 600,
-    update_limit: int = 10,
-):
-    """Filter 10Hz miniseed, 1 second, one minute, and temperature data.
-    Defaults set for realtime processing; can also be implemented to update legacy data"""
-    if input_factory == "miniseed":
-        input_factory = MiniSeedFactory(host=host, port=port)
-    elif input_factory == "edge":
-        input_factory = EdgeFactory(host=host, port=port)
-    if output_factory == "miniseed":
-        output_factory = MiniSeedFactory(
-            host=host, port=output_read_port, write_port=output_port
-        )
-    elif output_factory == "edge":
-        output_factory = EdgeFactory(
-            host=host, port=output_read_port, write_port=output_port
-        )
-
-    obsrio_tenhertz(
-        observatory=observatory,
-        input_factory=input_factory,
-        output_factory=output_factory,
-        realtime_interval=realtime_interval,
-        update_limit=update_limit,
-    )
-    obsrio_second(
-        observatory=observatory,
-        input_factory=input_factory,
-        output_factory=output_factory,
-        realtime_interval=realtime_interval,
-        update_limit=update_limit,
-    )
-    obsrio_minute(
-        observatory=observatory,
-        input_factory=input_factory,
-        output_factory=output_factory,
-        realtime_interval=realtime_interval,
-        update_limit=update_limit,
-    )
-    obsrio_temperatures(
-        observatory=observatory,
-        input_factory=input_factory,
-        output_factory=output_factory,
-        realtime_interval=realtime_interval,
-        update_limit=update_limit,
-    )
-
-
-def obsrio_day(
-    observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    output_factory: Optional[TimeseriesFactory] = None,
-    realtime_interval: int = 86400,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
     update_limit: int = 7,
+    channels: List[str] = ("U", "V", "W", "F"),
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
 ):
-    """Filter 1 second edge H,E,Z,F to 1 day miniseed U,V,W,F."""
-    starttime, endtime = get_realtime_interval(realtime_interval)
+    """ Filter 1 min UVWF to 1 day UVWF """
     controller = Controller(
-        inputFactory=input_factory or get_edge_factory(),
+        inputFactory=input_factory or get_miniseed_factory(interval="minute"),
+        outputFactory=output_factory or get_miniseed_factory(interval="day"),
         inputInterval="minute",
-        outputFactory=output_factory or get_miniseed_factory(),
         outputInterval="day",
     )
-    renames = {"H": "U", "E": "V", "Z": "W", "F": "F"}
-    for input_channel in renames.keys():
-        output_channel = renames[input_channel]
+    for channel in channels:
         controller.run_as_update(
+            observatory=(observatory,),
+            output_observatory=(observatory,),
             algorithm=FilterAlgorithm(
                 input_sample_period=60.0,
-                output_sample_period=86400.0,
-                inchannels=(input_channel,),
-                outchannels=(output_channel,),
+                output_sample_period=3600.0,
+                inchannels=(channel,),
+                outchannels=(channels,),
             ),
-            observatory=(observatory,),
-            output_observatory=(observatory,),
             starttime=starttime,
             endtime=endtime,
-            input_channels=(input_channel,),
-            output_channels=(output_channel,),
-            realtime=realtime_interval,
-            rename_output_channel=((input_channel, output_channel),),
+            input_channels=(channel,),
+            output_channels=(channel,),
+            realtime=endtime - starttime,
             update_limit=update_limit,
         )
 
 
-def obsrio_hour(
+def filter_hours(
     observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    output_factory: Optional[TimeseriesFactory] = None,
-    realtime_interval: int = 600,
-    update_limit: int = 10,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    update_limit: int = 24,
+    channels: List[str] = ("U", "V", "W", "F"),
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
 ):
-    """Filter 1 second edge H,E,Z,F to 1 hour miniseed U,V,W,F."""
-    starttime, endtime = get_realtime_interval(realtime_interval)
+    """ Filter 1 min UVWF to 1 hour UVWF """
     controller = Controller(
-        inputFactory=input_factory or get_edge_factory(),
+        inputFactory=input_factory or get_miniseed_factory(interval="minute"),
+        outputFactory=output_factory or get_miniseed_factory(interval="hour"),
         inputInterval="minute",
-        outputFactory=output_factory or get_miniseed_factory(),
         outputInterval="hour",
     )
-    renames = {"H": "U", "E": "V", "Z": "W", "F": "F"}
-    for input_channel in renames.keys():
-        output_channel = renames[input_channel]
+    for channel in channels:
         controller.run_as_update(
+            observatory=(observatory,),
+            output_observatory=(observatory,),
             algorithm=FilterAlgorithm(
                 input_sample_period=60.0,
                 output_sample_period=3600.0,
-                inchannels=(input_channel,),
-                outchannels=(output_channel,),
+                inchannels=(channel,),
+                outchannels=(channels,),
             ),
-            observatory=(observatory,),
-            output_observatory=(observatory,),
             starttime=starttime,
             endtime=endtime,
-            input_channels=(input_channel,),
-            output_channels=(output_channel,),
-            realtime=realtime_interval,
-            rename_output_channel=((input_channel, output_channel),),
+            input_channels=(channel,),
+            output_channels=(channel,),
+            realtime=endtime - starttime,
             update_limit=update_limit,
         )
 
 
-def obsrio_minute(
+def filter_minutes(
     observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    output_factory: Optional[TimeseriesFactory] = None,
-    realtime_interval: int = 600,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
     update_limit: int = 10,
+    channels: List[str] = ("U", "V", "W", "F"),
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
 ):
-    """Filter 1Hz legacy H,E,Z,F to 1 minute legacy.
-
-    Should be called after obsrio_second() and obsrio_tenhertz(),
-    which populate 1Hz legacy H,E,Z,F.
-    """
-    starttime, endtime = get_realtime_interval(realtime_interval)
+    """ Filter 1Hz UVWF to 1 min UVWF """
     controller = Controller(
-        inputFactory=input_factory or get_edge_factory(),
+        inputFactory=input_factory or get_miniseed_factory(),
+        outputFactory=output_factory or get_miniseed_factory(),
         inputInterval="second",
-        outputFactory=output_factory or get_edge_factory(),
         outputInterval="minute",
     )
-    for channel in ["H", "E", "Z", "F"]:
+    for channel in channels:
         controller.run_as_update(
+            observatory=(observatory,),
+            output_observatory=(observatory,),
             algorithm=FilterAlgorithm(
-                input_sample_period=1,
-                output_sample_period=60,
+                input_sample_period=1.0,
+                output_sample_period=60.0,
                 inchannels=(channel,),
                 outchannels=(channel,),
             ),
-            observatory=(observatory,),
-            output_observatory=(observatory,),
             starttime=starttime,
             endtime=endtime,
             input_channels=(channel,),
             output_channels=(channel,),
-            realtime=realtime_interval,
+            realtime=endtime - starttime,
             update_limit=update_limit,
         )
 
 
-def obsrio_second(
+def filter_temperatures(
     observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    output_factory: Optional[TimeseriesFactory] = None,
-    realtime_interval: int = 600,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
     update_limit: int = 10,
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
 ):
-    """Copy 1Hz miniseed F to 1Hz legacy F."""
-    starttime, endtime = get_realtime_interval(realtime_interval)
-    controller = Controller(
-        algorithm=Algorithm(inchannels=("F",), outchannels=("F",)),
-        inputFactory=input_factory or get_miniseed_factory(),
-        outputFactory=output_factory or get_edge_factory(),
-    )
-    controller.run_as_update(
-        observatory=(observatory,),
-        output_observatory=(observatory,),
-        starttime=starttime,
-        endtime=endtime,
-        input_channels=("F",),
-        output_channels=("F",),
-        realtime=realtime_interval,
-        update_limit=update_limit,
-    )
-
-
-def obsrio_temperatures(
-    observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    output_factory: Optional[TimeseriesFactory] = None,
-    realtime_interval: int = 600,
-    update_limit: int = 10,
-):
-    """Filter temperatures 1Hz miniseed (LK1-4) to 1 minute legacy (UK1-4)."""
-    starttime, endtime = get_realtime_interval(realtime_interval)
+    """ Filter 1Hz T1-4 to 1 min T1-4 """
     controller = Controller(
         inputFactory=input_factory or get_miniseed_factory(),
+        outputFactory=output_factory or get_miniseed_factory(),
         inputInterval="second",
-        outputFactory=output_factory or get_edge_factory(),
         outputInterval="minute",
     )
-    renames = {"LK1": "UK1", "LK2": "UK2", "LK3": "UK3", "LK4": "UK4"}
-    for input_channel in renames.keys():
-        output_channel = renames[input_channel]
+    input_channels = ["LK1", "LK2", "LK3", "LK4"]
+    output_channels = ["UK1", "UK2", "UK3", "UK4"]
+    for input_channel, output_channel in zip(input_channels, output_channels):
         controller.run_as_update(
+            observatory=(observatory,),
+            output_observatory=(observatory,),
             algorithm=FilterAlgorithm(
-                input_sample_period=1,
-                output_sample_period=60,
+                input_sample_period=1.0,
+                output_sample_period=60.0,
                 inchannels=(input_channel,),
                 outchannels=(output_channel,),
             ),
-            observatory=(observatory,),
-            output_observatory=(observatory,),
             starttime=starttime,
             endtime=endtime,
             input_channels=(input_channel,),
             output_channels=(output_channel,),
-            realtime=realtime_interval,
+            realtime=endtime - starttime,
             rename_output_channel=((input_channel, output_channel),),
             update_limit=update_limit,
         )
 
 
-def obsrio_tenhertz(
+def filter_tenhertz(
     observatory: str,
-    input_factory: Optional[TimeseriesFactory] = None,
-    output_factory: Optional[TimeseriesFactory] = None,
-    realtime_interval: int = 600,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
     update_limit: int = 10,
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
 ):
-    """Filter 10Hz miniseed U,V,W to 1Hz legacy H,E,Z."""
-    starttime, endtime = get_realtime_interval(realtime_interval)
+    """ Filter 10HZ UVW to 1Hz UVW """
     controller = Controller(
-        inputFactory=input_factory or get_miniseed_factory(),
+        inputFactory=input_factory
+        or get_miniseed_factory(convert_channels=("U", "V", "W")),
+        outputFactory=output_factory or get_miniseed_factory(),
         inputInterval="tenhertz",
-        outputFactory=output_factory or get_edge_factory(),
         outputInterval="second",
     )
-    renames = {"U": "H", "V": "E", "W": "Z"}
-    for input_channel in renames.keys():
-        output_channel = renames[input_channel]
+    for channel in ["U", "V", "W"]:
         controller.run_as_update(
+            observatory=(observatory,),
+            output_observatory=(observatory,),
             algorithm=FilterAlgorithm(
                 input_sample_period=0.1,
-                output_sample_period=1,
-                inchannels=(input_channel,),
-                outchannels=(output_channel,),
+                output_sample_period=1.0,
+                inchannels=(channel,),
+                outchannels=(channel,),
             ),
-            observatory=(observatory,),
-            output_observatory=(observatory,),
             starttime=starttime,
             endtime=endtime,
-            input_channels=(input_channel,),
-            output_channels=(output_channel,),
-            realtime=realtime_interval,
-            rename_output_channel=((input_channel, output_channel),),
+            input_channels=(channel,),
+            output_channels=(channel,),
+            realtime=endtime - starttime,
             update_limit=update_limit,
         )
+
+
+def prepare(
+    observatory: str,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    update_limit: int = 10,
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
+):
+    """Filter 10Hz UVW to 1Hz UVW
+    Filter 1Hz T1-4 to 1 min T1-4
+    Duplicate 1Hz W to Z
+    """
+    filter_tenhertz(
+        observatory=observatory,
+        starttime=starttime,
+        endtime=endtime,
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
+    )
+    input_factory.convert_channels = []
+    filter_temperatures(
+        observatory=observatory,
+        starttime=starttime,
+        endtime=endtime,
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
+    )
+    copy_channels(
+        observatory=observatory,
+        channels=[("W", "Z")],
+        starttime=starttime,
+        endtime=endtime,
+        data_interval="second",
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
+    )
diff --git a/geomagio/processing/pcdcp.py b/geomagio/processing/pcdcp.py
new file mode 100644
index 0000000000000000000000000000000000000000..f19670118e445b9c75ba2fa761e4011fbd56bb85
--- /dev/null
+++ b/geomagio/processing/pcdcp.py
@@ -0,0 +1,160 @@
+from typing import List, Optional, Tuple
+
+from obspy import UTCDateTime
+
+from ..algorithm import Algorithm
+from ..Controller import Controller
+from ..edge import EdgeFactory, MiniSeedFactory
+from .. import TimeseriesFactory
+from .factory import get_edge_factory, get_miniseed_factory
+
+
+def copy_channels(
+    observatory: str,
+    channels: List[Tuple[str]],
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    data_interval: str = "second",
+    update_limit: int = 10,
+    input_factory: Optional[TimeseriesFactory] = None,
+    output_factory: Optional[TimeseriesFactory] = None,
+):
+    """ Copy channels between legacy and miniseed formats """
+    controller = Controller(
+        inputFactory=input_factory or get_edge_factory(),
+        outputFactory=output_factory or get_miniseed_factory(),
+        inputInterval=data_interval,
+        outputInterval=data_interval,
+    )
+    for input_channel, output_channel in channels:
+        controller.run_as_update(
+            algorithm=Algorithm(
+                inchannels=input_channel,
+                outchannels=output_channel,
+            ),
+            observatory=(observatory,),
+            output_observatory=(observatory,),
+            starttime=starttime,
+            endtime=endtime,
+            input_channels=(input_channel,),
+            output_channels=(output_channel,),
+            rename_output_channel=((input_channel, output_channel),),
+            realtime=endtime - starttime,
+            update_limit=update_limit,
+        )
+
+
+def legacy_adjusted(
+    observatory: str,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    update_limit: int = 10,
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[EdgeFactory] = None,
+):
+    """ Copy 1Hz/1 min derived adjusted channels from miniseed to legacy """
+    input_factory = input_factory or get_miniseed_factory(data_type="adjusted")
+    output_factory = output_factory or get_edge_factory(data_type="adjusted")
+    copy_channels(
+        observatory=observatory,
+        channels=[
+            ("X", "X"),
+            ("Y", "Y"),
+            ("Z", "Z"),
+            ("F", "F"),
+        ],
+        starttime=starttime,
+        endtime=endtime,
+        data_interval="second",
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
+    )
+    copy_channels(
+        observatory=observatory,
+        channels=[
+            ("X", "X"),
+            ("Y", "Y"),
+            ("Z", "Z"),
+            ("F", "F"),
+            ("H_SQ", "MSQ"),
+            ("H_SV", "MSV"),
+            ("H_Dist", "MDT"),
+        ],
+        starttime=starttime,
+        endtime=endtime,
+        data_interval="minute",
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
+    )
+
+
+def legacy_variation(
+    observatory: str,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    update_limit: int = 10,
+    input_factory: Optional[MiniSeedFactory] = None,
+    output_factory: Optional[EdgeFactory] = None,
+):
+    """ Copy 1Hz/1 min derived variation channels from miniseed to legacy """
+    input_factory = input_factory or get_miniseed_factory(data_type="variation")
+    output_factory = output_factory or get_edge_factory(data_type="variation")
+    copy_channels(
+        observatory=observatory,
+        channels=[
+            ("H_SQ", "MSQ"),
+            ("H_SV", "MSV"),
+            ("H_Dist", "MDT"),
+        ],
+        starttime=starttime,
+        endtime=endtime,
+        data_interval="minute",
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
+    )
+
+
+def prepare(
+    observatory: str,
+    starttime: UTCDateTime,
+    endtime: UTCDateTime,
+    update_limit: int = 10,
+    input_factory: Optional[EdgeFactory] = None,
+    output_factory: Optional[MiniSeedFactory] = None,
+):
+    """Copy 1Hz legacy HEZF to 1Hz miniseed UVWF
+    Copy 1 min T1-4 from legacy to miniseed
+    """
+    copy_channels(
+        observatory=observatory,
+        channels=[
+            ("H", "U"),
+            ("E", "V"),
+            ("Z", "W"),
+            ("F", "F"),
+        ],
+        starttime=starttime,
+        endtime=endtime,
+        data_interval="second",
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
+    )
+    copy_channels(
+        observatory=observatory,
+        channels=[
+            ("UK1", "UK1"),
+            ("UK2", "UK2"),
+            ("UK3", "UK3"),
+            ("UK4", "UK4"),
+        ],
+        starttime=starttime,
+        endtime=endtime,
+        data_interval="minute",
+        update_limit=update_limit,
+        input_factory=input_factory,
+        output_factory=output_factory,
+    )
diff --git a/setup.py b/setup.py
index 0d7801818eb84eaf21ae6c77c8b308409cf748ed..2413e901186984bf715fcfd399b0677291bb2b84 100644
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ setuptools.setup(
             "generate-matrix=geomagio.processing.affine_matrix:main",
             "geomag-metadata=geomagio.metadata.main:main",
             "magproc-prepfiles=geomagio.processing.magproc:main",
-            "obsrio-filter=geomagio.processing.obsrio:main",
+            "observatory=geomagio.processing.observatory:main",
         ],
     },
 )
diff --git a/test/ChannelConverter_test.py b/test/ChannelConverter_test.py
index bf050888bd9229c4fd2e311f81463975af8a1591..79ae68a2b6add67f32dd134c3eedb99381e35e77 100644
--- a/test/ChannelConverter_test.py
+++ b/test/ChannelConverter_test.py
@@ -21,9 +21,9 @@ class ChannelConverterTest:
     _____
 
     Observatory frame of reference.
-        h: the component corresponding to the field strength along the
+        u: the component corresponding to the field strength along the
             observatories primary horizontal axis.
-        e: the component corresponding to the field strength along the
+        v: the component corresponding to the field strength along the
             observatories secondary horizonal axis.
         d: the angle between observatory h and the horizontal field value
             (aka magnetic north in the horizontal plane.)
@@ -50,59 +50,59 @@ class ChannelConverterTest:
 
         Computed f is the combination of the 3 vector components from
             either the geographic coordinate system (X,Y,Z), or the observatory
-            coordinate system (h,e,z).
+            coordinate system (u,v,w).
         """
-        h = 2
-        e = 2
-        z = 2
-        fv = channel.get_computed_f_using_squares(h, e, z)
+        u = 2
+        v = 2
+        w = 2
+        fv = channel.get_computed_f_using_squares(u, v, w)
         fs = math.sqrt(12)
         assert_almost_equal(fv, fs, 8, "Expect fv to almost equal sqrt(12)", True)
 
     def test_get_geo_from_obs(self):
         """ChannelConverter_test.test_get_geo_from_obs()
 
-        The observatory component ``h`` and ``e`` combined with the declination
+        The observatory component ``u`` and ``v`` combined with the declination
         basline angle ``d0`` converts to the geographic north component
         ``X`` and east vector ``Y``
         """
 
-        # 1) Call get_geo_from_obs using equal h,e values with a d0 of 0
+        # 1) Call get_geo_from_obs using equal u,v values with a d0 of 0
         #   the geographic values X,Y will be the same. This proves the simple
         #   case where the observatory is aligned with geographic north.
-        h = 1
-        e = 1
-        (X, Y) = channel.get_geo_from_obs(h, e)
+        u = 1
+        v = 1
+        (X, Y) = channel.get_geo_from_obs(u, v)
         assert_almost_equal(X, 1, 8, "Expect X to almost equal 1.", True)
         assert_almost_equal(Y, 1, 8, "Expect Y to almost equal 1.", True)
 
-        # 2) Call get_geo_from_obs using h,e values of cos(15), sin(15)
+        # 2) Call get_geo_from_obs using u,v values of cos(15), sin(15)
         #       (to create a d of 15 degrees) and a d0 of 15 degrees.
         #       X and Y will be cos(30), sin(30)
-        h = cos(15 * D2R)
-        e = sin(15 * D2R)
+        u = cos(15 * D2R)
+        v = sin(15 * D2R)
         d0 = 15 * D2R
-        (X, Y) = channel.get_geo_from_obs(h, e, d0)
+        (X, Y) = channel.get_geo_from_obs(u, v, d0)
         assert_almost_equal(X, cos(30 * D2R), 8, "Expect X to equal cos(30)", True)
         assert_almost_equal(Y, sin(30 * D2R), 8, "Expect Y to equal sin(30)", True)
 
-        # 3) Call get_geo_from_obs using h,e values of 1,0 with a d0 of 315
+        # 3) Call get_geo_from_obs using u,v values of 1,0 with a d0 of 315
         #   degrees. The geographic X,Y will be cos(45), sin(-45)
-        h = 1
-        e = 0
+        u = 1
+        v = 0
         d0 = 315 * D2R
-        (X, Y) = channel.get_geo_from_obs(h, e, d0)
+        (X, Y) = channel.get_geo_from_obs(u, v, d0)
         assert_almost_equal(X, cos(45 * D2R), 8, "Expect X to equal cos(45).", True)
         assert_almost_equal(Y, sin(-45 * D2R), 8, "Expect Y to equal sin(45).", True)
 
-        # 4) Call get_geo_from_obs using h,e values of cos_30,sin_30 and d0 of
+        # 4) Call get_geo_from_obs using u,v values of cos_30,sin_30 and d0 of
         #   30 degrees. The geographic X,Y will be cos(-30), sin(-30), due to
         #   combined angle of the observatory declination of 30 degrees, and
         #   the d0 of -60 degrees.
-        h = cos(30 * D2R)
-        e = sin(30 * D2R)
+        u = cos(30 * D2R)
+        v = sin(30 * D2R)
         d0 = -60 * D2R
-        (X, Y) = channel.get_geo_from_obs(h, e, d0)
+        (X, Y) = channel.get_geo_from_obs(u, v, d0)
         assert_almost_equal(X, cos(-30 * D2R), 8, "Expect X to equal cos(60).", True)
         assert_almost_equal(Y, sin(-30 * D2R), 8, "Expect Y to equal sin(60).", True)
 
@@ -164,19 +164,19 @@ class ChannelConverterTest:
     def test_get_mag_from_obs(self):
         """ChannelConverter_test.test_get_geo_y_from_obs()
 
-        ``h``, ``e`` are the primary and secondary axis of the ``H``
+        ``u``, ``v`` are the primary and secondary axis of the ``H``
         vector in the horizontal plane of the magnetic field.  ``d0``
         is the declination baseline of the observatory frame of reference.
         ``D`` comes from the combination of ``d0`` and the angle produced
-        from the ``h`` and ``e`` components.
+        from the ``u`` and ``v`` components.
         """
 
-        # Call get_mag_from_obs using h,d of cos(30), sin(30) and
+        # Call get_mag_from_obs using u,v of cos(30), sin(30) and
         #   d0 of 15 degrees. Expect H,D to equal 1, 45.
-        h = cos(30 * D2R)
-        e = sin(30 * D2R)
+        u = cos(30 * D2R)
+        v = sin(30 * D2R)
         d0 = 15 * D2R
-        H, D = channel.get_mag_from_obs(h, e, d0)
+        H, D = channel.get_mag_from_obs(u, v, d0)
         assert_almost_equal(H, 1, 8, "Expect H to be 1.", True)
         assert_almost_equal(D, 45 * D2R, 8, "Expect D to be 45.", True)
 
@@ -199,51 +199,51 @@ class ChannelConverterTest:
     def test_get_mag_d_from_obs(self):
         """ChannelConverter_test.test_get_mag_d_from_obs()
 
-        The observatory components ``h`` and ``e`` form an angle (d) with
+        The observatory components ``u`` and ``v`` form an angle (d) with
         the horizontal magnetic vector. Adding d to the observatory
         declination baseline ``d0`` the magnetic declination angle ``D`` can
         be found.
         """
 
-        # 1) Call get_mag_d_from_obs using h,e equal to each other.
+        # 1) Call get_mag_d_from_obs using u,v equal to each other.
         #   Expect D to be 45 degrees.
-        h = 2
-        e = 2
-        D = channel.get_mag_d_from_obs(h, e)
+        u = 2
+        v = 2
+        D = channel.get_mag_d_from_obs(u, v)
         assert_almost_equal(D, 45 * D2R, 8, "Expect D to be 45 degrees.", True)
-        # 2) Call get_mag_d_from_obs using h,e cos(30), sin(30).
+        # 2) Call get_mag_d_from_obs using u,v cos(30), sin(30).
         #   Expect d of 30 degress.
-        h = cos(30 * D2R)
-        e = sin(30 * D2R)
-        D = channel.get_mag_d_from_obs(h, e)
+        u = cos(30 * D2R)
+        v = sin(30 * D2R)
+        D = channel.get_mag_d_from_obs(u, v)
         assert_almost_equal(D, 30 * D2R, 8, "Expect D to equal 30 degrees", True)
-        # 3) Call get_mag_d_from_obs using h,e cos(30), sin(30),
+        # 3) Call get_mag_d_from_obs using u,v cos(30), sin(30),
         #   d0 = 30 degrees Expect d to be 60 degress.
-        h = cos(30 * D2R)
-        e = sin(30 * D2R)
+        u = cos(30 * D2R)
+        v = sin(30 * D2R)
         d0 = 30 * D2R
-        D = channel.get_mag_d_from_obs(h, e, d0)
+        D = channel.get_mag_d_from_obs(u, v, d0)
         assert_almost_equal(D, 60 * D2R, 8, "Expect D to equal 60 degrees", True)
-        # 4) Call get_mag_d_from_obs using h,e cos(30), sin(30),
+        # 4) Call get_mag_d_from_obs using u,v cos(30), sin(30),
         #   d0 = 330 degrees Expect d of 360 degress.
-        h = cos(30 * D2R)
-        e = sin(30 * D2R)
+        u = cos(30 * D2R)
+        v = sin(30 * D2R)
         d0 = 330 * D2R
-        D = channel.get_mag_d_from_obs(h, e, d0)
+        D = channel.get_mag_d_from_obs(u, v, d0)
         assert_almost_equal(D, 360 * D2R, 8, "Expect D to equal 360 degrees", True)
-        # 5) Call get_mag_d_from_obs using h,e cos(30), sin(30),
+        # 5) Call get_mag_d_from_obs using u,v cos(30), sin(30),
         #   d0 = -30 degrees Expect d of 0 degress.
-        h = cos(30 * D2R)
-        e = sin(30 * D2R)
+        u = cos(30 * D2R)
+        v = sin(30 * D2R)
         d0 = -30 * D2R
-        D = channel.get_mag_d_from_obs(h, e, d0)
+        D = channel.get_mag_d_from_obs(u, v, d0)
         assert_almost_equal(D, 0, 8, "Expect D to equal 0 degrees", True)
-        # 6) Call get_mag_d_from_obs using h,e cos(30), -sin(30),
+        # 6) Call get_mag_d_from_obs using u,v cos(30), -sin(30),
         #   d0 = -30 degrees. Expect d of -60 degress.
-        h = cos(30 * D2R)
-        e = sin(-30 * D2R)
+        u = cos(30 * D2R)
+        v = sin(-30 * D2R)
         d0 = -30 * D2R
-        D = channel.get_mag_d_from_obs(h, e, d0)
+        D = channel.get_mag_d_from_obs(u, v, d0)
         assert_almost_equal(D, -60 * D2R, 8, "Expect D to equal -60 degrees", True)
 
     def test_get_mag_d_from_geo(self):
@@ -303,17 +303,17 @@ class ChannelConverterTest:
 
         The geographic north and east components ``X`` and ``Y`` of the
         magnetic field vector H, combined with the declination baseline angle
-        ``d0`` can be used to produce the observatory components ``h`` and
-        ```e``
+        ``d0`` can be used to produce the observatory components ``u`` and
+        ```v``
         """
 
         # 1) Call get_obs_from_geo using equal X,Y values with a d0 of 0
-        #   the observatory values h,e will be the same.
+        #   the observatory values u,v will be the same.
         X = 1
         Y = 1
-        (h, e) = channel.get_obs_from_geo(X, Y)
-        assert_almost_equal(h, 1.0, 8, "Expect h to be 1.", True)
-        assert_almost_equal(e, 1.0, 8, "Expect e to be 1.", True)
+        (u, v) = channel.get_obs_from_geo(X, Y)
+        assert_almost_equal(u, 1.0, 8, "Expect u to be 1.", True)
+        assert_almost_equal(v, 1.0, 8, "Expect v to be 1.", True)
         # 2) Call get_obs_from_geo using equal X,Y values to create a 45
         #   degree angle (D), with a d0 of 45/2. The observatory declination
         #   (d) will be 45/2, the difference between the total field angle,
@@ -321,8 +321,8 @@ class ChannelConverterTest:
         X = 1
         Y = 1
         d0 = 22.5 * D2R
-        (h, e) = channel.get_obs_from_geo(X, Y, d0)
-        d = channel.get_obs_d_from_obs(h, e)
+        (u, v) = channel.get_obs_from_geo(X, Y, d0)
+        d = channel.get_obs_d_from_obs(u, v)
         assert_almost_equal(d, 22.5 * D2R, 8, "Expect d to be 22.5 degrees.", True)
         # 3) Call get_obs_from_geo using equal X,Y values to create a 45
         #   degree angle (D), with a d0 of 315 degrees. The observatory
@@ -330,21 +330,21 @@ class ChannelConverterTest:
         X = 1
         Y = 1
         d0 = 315 * D2R
-        (h, e) = channel.get_obs_from_geo(X, Y, d0)
-        d = channel.get_obs_d_from_obs(h, e)
+        (u, v) = channel.get_obs_from_geo(X, Y, d0)
+        d = channel.get_obs_d_from_obs(u, v)
         assert_almost_equal(d, 90 * D2R, 8, "Expect d to be 90 degrees.", True)
         # 4) Call get_obs_from_geo using X,Y values of cos(60), sin(60), and
-        #   d0 of 30 degrees. The observatory values h,e will be cos(30)
+        #   d0 of 30 degrees. The observatory values u,v will be cos(30)
         #   and sin(30), and the observatory declination will be 30 degrees.
         #   The observatory angle of 30 degrees + the d0 of 30 degrees produces
         #   the total declination (D) of 60 degrees.
         X = cos(60 * D2R)
         Y = sin(60 * D2R)
         d0 = 30 * D2R
-        (h, e) = channel.get_obs_from_geo(X, Y, d0)
-        assert_almost_equal(h, cos(30 * D2R), 8, "Expect h to be cos(30).", True)
-        assert_almost_equal(e, sin(30 * D2R), 8, "Expect e to be sin(30).", True)
-        d = channel.get_obs_d_from_obs(h, e)
+        (u, v) = channel.get_obs_from_geo(X, Y, d0)
+        assert_almost_equal(u, cos(30 * D2R), 8, "Expect u to be cos(30).", True)
+        assert_almost_equal(v, sin(30 * D2R), 8, "Expect v to be sin(30).", True)
+        d = channel.get_obs_d_from_obs(u, v)
         assert_almost_equal(d, 30 * D2R, 8, "Expect d to be 30 degrees.", True)
 
     def test_get_obs_from_mag(self):
@@ -357,28 +357,28 @@ class ChannelConverterTest:
 
         H = 1
         D = -22.5 * D2R
-        (h, e) = channel.get_obs_from_mag(H, D, 22.5 * D2R)
-        assert_almost_equal(h, cos(45 * D2R), 8, "Expect h to be cos(45)", True)
-        assert_almost_equal(e, -cos(45 * D2R), 8, "Expect e to be -cos(45).", True)
+        (u, v) = channel.get_obs_from_mag(H, D, 22.5 * D2R)
+        assert_almost_equal(u, cos(45 * D2R), 8, "Expect u to be cos(45)", True)
+        assert_almost_equal(v, -cos(45 * D2R), 8, "Expect v to be -cos(45).", True)
 
     def test_get_obs_d_from_obs(self):
         """ChannelConverter_test.test_get_obs_d_from_obs()
 
-        ``d`` is the angle formed by the observatory components ``h`` and
-        ``e`` the primary and secondary axis of the horizontal magnetic
+        ``d`` is the angle formed by the observatory components ``u`` and
+        ``v`` the primary and secondary axis of the horizontal magnetic
         field vector in the observatories frame of reference.
         """
-        # 1) Call get_obs_d_from_obs usine h,e equal to cos(30), sin(30).
+        # 1) Call get_obs_d_from_obs usine u,v equal to cos(30), sin(30).
         #   Expect d to be 30.
-        h = cos(30 * D2R)
-        e = sin(30 * D2R)
-        d = channel.get_obs_d_from_obs(h, e)
+        u = cos(30 * D2R)
+        v = sin(30 * D2R)
+        d = channel.get_obs_d_from_obs(u, v)
         assert_almost_equal(d, 30 * D2R, 8, "Expect d to be 30 degrees.", True)
-        # 2) Call get_obs_d_from_obs using h,e cos(30), -sin(30). Expect
+        # 2) Call get_obs_d_from_obs using u,v cos(30), -sin(30). Expect
         #   d to be 30.
-        h = cos(30 * D2R)
-        e = sin(-30 * D2R)
-        d = channel.get_obs_d_from_obs(h, e)
+        u = cos(30 * D2R)
+        v = sin(-30 * D2R)
+        d = channel.get_obs_d_from_obs(u, v)
         assert_almost_equal(d, -30 * D2R, 8, "Expect d to be 30 degrees.", True)
 
     def test_get_obs_d_from_mag_d(self):
@@ -416,69 +416,69 @@ class ChannelConverterTest:
     def test_get_obs_e_from_mag(self):
         """ChannelConverter_test.test_get_obs_e_from_mag()
 
-        ``e`` is the secondary axis or 'east' component of the observatory
+        ``v`` is the secondary axis or 'east' component of the observatory
         reference frame. Using the difference between the magnetic declination
         angle ``D`` and the observatory baseline declination ``d0`` to get the
-        observatory declination angle d the ``e`` component can be found
+        observatory declination angle d the ``v`` component can be found
         as ``H`` * sin(d)
         """
 
-        # 1) Call get_obs_e_from mag using H,D of 1,45.  Expect e to be sin(45)
+        # 1) Call get_obs_v_from_mag using H,D of 1,45.  Expect v to be sin(45)
         H = 1
         D = 45 * D2R
-        e = channel.get_obs_e_from_mag(H, D)
-        assert_almost_equal(e, sin(45 * D2R), 8, "Expect e to be sin(45).", True)
-        # 2) Call get_obs_e_from_mag using H,D of 1, 30. Expect e to be sin(30)
+        v = channel.get_obs_v_from_mag(H, D)
+        assert_almost_equal(v, sin(45 * D2R), 8, "Expect v to be sin(45).", True)
+        # 2) Call get_obs_v_from_mag using H,D of 1, 30. Expect v to be sin(30)
         H = 1
         D = 30 * D2R
-        e = channel.get_obs_e_from_mag(H, D)
-        assert_almost_equal(e, sin(30 * D2R), 8, "Expect e to be sin(30).", True)
-        # 3) Call get_obs_e_from_mag using H,D,d0 of 1, 15, -15. Expect e to
+        v = channel.get_obs_v_from_mag(H, D)
+        assert_almost_equal(v, sin(30 * D2R), 8, "Expect v to be sin(30).", True)
+        # 3) Call get_obs_v_from_mag using H,D,d0 of 1, 15, -15. Expect v to
         #   be sin(30)
         H = 1
         D = 15 * D2R
         d0 = -15 * D2R
-        e = channel.get_obs_e_from_mag(H, D, d0)
-        assert_almost_equal(e, sin(30 * D2R), 8, "Expect e to be sin(30)", True)
+        v = channel.get_obs_v_from_mag(H, D, d0)
+        assert_almost_equal(v, sin(30 * D2R), 8, "Expect v to be sin(30)", True)
 
     def test_get_obs_e_from_obs(self):
         """ChannelConverter_test.test_get_obs_e_from_obs()
 
-        ``e`` is the seconday (east) component of the observatory vector ``h``.
-        ``e`` is calculated using ``h`` * tan(``d``) where ``d`` is the
+        ``v`` is the seconday (east) component of the observatory vector ``u``.
+        ``v`` is calculated using ``u`` * tan(``d``) where ``d`` is the
         declination angle of the observatory.
         """
 
         # Call get_obs_e_from_obs using h,d of 2, 30.
-        #   Expect e to be 2 * tan(30)
+        #   Expect v to be 2 * tan(30)
         h = 2
         d = 30 * D2R
-        e = channel.get_obs_e_from_obs(h, d)
+        v = channel.get_obs_v_from_obs(h, d)
         assert_almost_equal(
-            e, 2 * tan(30 * D2R), 8, "Expect e to be 2 * tan(30).", True
+            v, 2 * tan(30 * D2R), 8, "Expect v to be 2 * tan(30).", True
         )
 
-    def test_get_obs_h_from_mag(self):
-        """ChannelConverter_test.test_get_obs_h_from_mag()
+    def test_get_obs_u_from_mag(self):
+        """ChannelConverter_test.test_get_obs_u_from_mag()
 
-        The observatories horizontal magnetic vector ``h`` is caculated from
+        The observatories horizontal magnetic vector ``u`` is caculated from
         the magnetic north vector ``H`` and the observatory declination angle
         ``d``.  ``d`` is the difference of the magnetic declination ``D`` and
         the observatory baseline declination ``d0``
         """
 
-        # 1) Call get_obs_h_from_mag using H,D 1,45. Expect h to be cos(45)
+        # 1) Call get_obs_u_from_mag using H,D 1,45. Expect u to be cos(45)
         H = 1
         D = 45 * D2R
-        h = channel.get_obs_h_from_mag(H, D)
-        assert_almost_equal(h, cos(45 * D2R), 8, "Expect h to be cos(45).", True)
-        # 2) Call get_obs_h_from_mag using H,D,d0 1,30,15.
-        #   Expect h to be cos(15)
+        u = channel.get_obs_u_from_mag(H, D)
+        assert_almost_equal(u, cos(45 * D2R), 8, "Expect u to be cos(45).", True)
+        # 2) Call get_obs_u_from_mag using H,D,d0 1,30,15.
+        #   Expect u to be cos(15)
         H = 1
         D = 30 * D2R
         d0 = 15 * D2R
-        h = channel.get_obs_h_from_mag(H, D, d0)
-        assert_almost_equal(h, cos(15 * D2R), 8, "Expect h to be cos(15)", True)
+        u = channel.get_obs_u_from_mag(H, D, d0)
+        assert_almost_equal(u, cos(15 * D2R), 8, "Expect u to be cos(15)", True)
 
     def test_geo_to_obs_to_geo(self):
         """ChannelConverter_test.test_geo_to_obs_to_geo()
@@ -488,14 +488,14 @@ class ChannelConverterTest:
             get_geo_from_obs. Expect the end values to be the same
             as the start values.
         """
-        h_in = 20840.15
-        e_in = -74.16
+        u_in = 20840.15
+        v_in = -74.16
         d0 = dec_bas_rad
-        (X, Y) = channel.get_geo_from_obs(h_in, e_in, d0)
-        (h, e) = channel.get_obs_from_geo(X, Y, d0)
+        (X, Y) = channel.get_geo_from_obs(u_in, v_in, d0)
+        (u, v) = channel.get_obs_from_geo(X, Y, d0)
 
-        assert_almost_equal(h, 20840.15, 8, "Expect h to = 20840.15.", True)
-        assert_almost_equal(e, -74.16, 8, "Expect e to = -74.16", True)
+        assert_almost_equal(u, 20840.15, 8, "Expect u to = 20840.15.", True)
+        assert_almost_equal(v, -74.16, 8, "Expect v to = -74.16", True)
 
     def test_get_radians_from_minutes(self):
         """ChannelConverter_test.test_get_radian_from_decimal()
diff --git a/test/StreamConverter_test.py b/test/StreamConverter_test.py
index 0c8b4207ac380f9d27efb5c526d24f9703dcec78..4421411516715a3bf73662d0dba8c0048550b451 100644
--- a/test/StreamConverter_test.py
+++ b/test/StreamConverter_test.py
@@ -63,16 +63,16 @@ def test_get_geo_from_obs():
     """StreamConverter_test.test_get_geo_from_obs()
 
     The observatory stream containing the observatory traces
-    ''h'', ''d'' or ''e'', ''z'', and ''f'' converts to the geographic
+    ''u'', ''d'' or ''v'', ''w'', and ''f'' converts to the geographic
     stream containing the traces ''x'', ''y'', ''z'', and ''f''
     """
     obs = obspy.core.Stream()
 
-    # 1) Call get_geo_from_obs using equal h, e streams with a decbas of 0
+    # 1) Call get_geo_from_obs using equal u, v streams with a decbas of 0
     #   the geographic stream values X, Y will be the same.
-    obs += __create_trace("H", [1])
-    obs += __create_trace("E", [1])
-    obs += __create_trace("Z", [1])
+    obs += __create_trace("U", [1])
+    obs += __create_trace("V", [1])
+    obs += __create_trace("W", [1])
     obs += __create_trace("F", [1])
     geo = StreamConverter.get_geo_from_obs(obs)
     X = geo.select(channel="X")[0].data
@@ -81,13 +81,13 @@ def test_get_geo_from_obs():
     assert_almost_equal(Y[0], 1, 9, "Expect Y to almost equal 1", True)
 
     # 2) Call get_geo_from_obs using a decbas of 15 degrees, and streams
-    #   with H = [cos(15), cos(30)], and E = [sin(15), sin(30)].
+    #   with U = [cos(15), cos(30)], and V = [sin(15), sin(30)].
     #   Expect streams of X = [cos(30), cos(45)] and Y = sin(30), sin(45)
     obs = obspy.core.Stream()
     DECBAS = 15 * D2I
-    obs += __create_trace("H", [cos(15 * D2R), cos(30 * D2R)], DECBAS)
-    obs += __create_trace("E", [sin(15 * D2R), sin(30 * D2R)], DECBAS)
-    obs += __create_trace("Z", [1, 1], DECBAS)
+    obs += __create_trace("U", [cos(15 * D2R), cos(30 * D2R)], DECBAS)
+    obs += __create_trace("V", [sin(15 * D2R), sin(30 * D2R)], DECBAS)
+    obs += __create_trace("W", [1, 1], DECBAS)
     obs += __create_trace("F", [1, 1], DECBAS)
     geo = StreamConverter.get_geo_from_obs(obs)
     X = geo.select(channel="X")[0].data
@@ -137,18 +137,18 @@ def test_get_mag_from_geo():
 def test_get_mag_from_obs():
     """StreamConverter_test.test_get_mag_from_obs()
 
-    The observatory stream containing the traces ''h'', ''e'' or ''d'',
-        ''z'' and ''f''
+    The observatory stream containing the traces ''u'', ''v'' or ''d'',
+        ''w'' and ''f''
     """
     obs = obspy.core.Stream()
 
-    # Call get_mag_from_obs using a DECBAS of 15 degrees, a H stream of
-    #   [cos(15), cos(30)] and a E stream of [sin(15), sin(30)].
+    # Call get_mag_from_obs using a DECBAS of 15 degrees, a U stream of
+    #   [cos(15), cos(30)] and a V stream of [sin(15), sin(30)].
     #   Expect a H stream of [1, 1] and a D stream of [30 degrees, 45 degrees]
     DECBAS = 15 * D2I
-    obs += __create_trace("H", [cos(15 * D2R), cos(30 * D2R)], DECBAS)
-    obs += __create_trace("E", [sin(15 * D2R), sin(30 * D2R)], DECBAS)
-    obs += __create_trace("Z", [1, 1], DECBAS)
+    obs += __create_trace("U", [cos(15 * D2R), cos(30 * D2R)], DECBAS)
+    obs += __create_trace("V", [sin(15 * D2R), sin(30 * D2R)], DECBAS)
+    obs += __create_trace("W", [1, 1], DECBAS)
     obs += __create_trace("F", [1, 1], DECBAS)
     mag = StreamConverter.get_mag_from_obs(obs)
     H = mag.select(channel="H")[0].data
@@ -164,35 +164,35 @@ def test_get_obs_from_geo():
 
     The geographic stream containing the traces ''x'', ''y'', ''z'', and
     ''f'' converts to the observatory stream containing the traces
-    ''h'', ''d'' or ''e'', ''z'', and ''f''.
+    ''u'', ''d'' or ''v'', ''w'', and ''f''.
     """
     geo = obspy.core.Stream()
 
     # Call get_geo_from_obs using a decbas of 15, a X stream of
     #   [cos(30), cos(45)], and a Y stream of [sin(30), sin(45)].
-    #   Expect a H stream of [cos(15), cos(30)] and a
-    #   E stream of [sin(15), sin(30)]
+    #   Expect a U stream of [cos(15), cos(30)] and a
+    #   V stream of [sin(15), sin(30)]
     DECBAS = 15 * D2I
     geo += __create_trace("X", [cos(30 * D2R), cos(45 * D2R)], DECBAS)
     geo += __create_trace("Y", [sin(30 * D2R), sin(45 * D2R)], DECBAS)
     geo += __create_trace("Z", [1, 1], DECBAS)
     geo += __create_trace("F", [1, 1], DECBAS)
     obs = StreamConverter.get_obs_from_geo(geo, True)
-    H = obs.select(channel="H")[0].data
-    E = obs.select(channel="E")[0].data
+    U = obs.select(channel="U")[0].data
+    V = obs.select(channel="V")[0].data
     D = obs.select(channel="D")[0].data
     assert_almost_equal(
-        H,
+        U,
         [cos(15 * D2R), cos(30 * D2R)],
         9,
-        "Expect H to equal [cos(15), cos(30)]",
+        "Expect U to equal [cos(15), cos(30)]",
         True,
     )
     assert_almost_equal(
-        E,
+        V,
         [sin(15 * D2R), sin(30 * D2R)],
         9,
-        "Expect E to equal [sin(15), sin(30)",
+        "Expect V to equal [sin(15), sin(30)",
         True,
     )
     assert_almost_equal(
@@ -205,38 +205,38 @@ def test_get_obs_from_mag():
 
     The magnetic stream containing the traces ''h'', ''d'', ''z'', and ''f''
         converts to the observatory stream containing the traces
-        ''h'', ''e'' and/or ''d'', ''z'', and ''f''
+        ''u'', ''v'' and/or ''d'', ''w'', and ''f''
     """
     mag = obspy.core.Stream()
 
     # Call get_obs_from_mag using a decbas of 15, a H stream of [1,1],
-    #   and a D stream of [30 degrees, 45 degrees]. Expect a H stream
+    #   and a D stream of [30 degrees, 45 degrees]. Expect a U stream
     #   of [cos(15), cos(30)], a D stream of [30 degrees, 45 degrees],
-    #   and a E stream of [sin(15), sin(30)]
+    #   and a V stream of [sin(15), sin(30)]
     DECBAS = 15 * D2I
     mag += __create_trace("H", [1, 1], DECBAS)
     mag += __create_trace("D", [30 * D2R, 45 * D2R], DECBAS)
     mag += __create_trace("Z", [1, 1], DECBAS)
     mag += __create_trace("F", [1, 1], DECBAS)
     obs = StreamConverter.get_obs_from_mag(mag, True)
-    H = obs.select(channel="H")[0].data
+    U = obs.select(channel="U")[0].data
     D = obs.select(channel="D")[0].data
-    E = obs.select(channel="E")[0].data
+    V = obs.select(channel="V")[0].data
     assert_almost_equal(
-        H,
+        U,
         [cos(15 * D2R), cos(30 * D2R)],
         9,
-        "Expect H to equal [cos(15), cos(30)",
+        "Expect U to equal [cos(15), cos(30)",
         True,
     )
     assert_almost_equal(
         D, [15 * D2R, 30 * D2R], 9, "Expect D to equal [15 degrees, 30 degrees", True
     )
     assert_almost_equal(
-        E,
+        V,
         [sin(15 * D2R), sin(30 * D2R)],
         9,
-        "Expect E to equal [sin(15), sin(30)",
+        "Expect V to equal [sin(15), sin(30)",
         True,
     )
 
@@ -244,19 +244,19 @@ def test_get_obs_from_mag():
 def test_get_obs_from_obs():
     """StreamConverter_test.test_get_obs_from_obs()
 
-    The observatory stream can contain either ''d'' or ''e'' depending
+    The observatory stream can contain either ''d'' or ''v'' depending
     on it's source. get_obs_from_obs will return either or both as part
     of the obs Stream.
     """
 
-    # 1) Call get_obs_from_obs using a decbas of 15, a H stream of
-    #   [cos(15), cos(30)], and a E stream of [sin(15), sin(30)].
+    # 1) Call get_obs_from_obs using a decbas of 15, a U stream of
+    #   [cos(15), cos(30)], and a V stream of [sin(15), sin(30)].
     #   Expect a D stream of [15 degrees, 30 degrees]
     obs_e = obspy.core.Stream()
     DECBAS = 15 * D2I
-    obs_e += __create_trace("H", [cos(15 * D2R), cos(30 * D2R)], DECBAS)
-    obs_e += __create_trace("E", [sin(15 * D2R), sin(30 * D2R)], DECBAS)
-    obs_e += __create_trace("Z", [1, 1], DECBAS)
+    obs_e += __create_trace("U", [cos(15 * D2R), cos(30 * D2R)], DECBAS)
+    obs_e += __create_trace("V", [sin(15 * D2R), sin(30 * D2R)], DECBAS)
+    obs_e += __create_trace("W", [1, 1], DECBAS)
     obs_e += __create_trace("F", [1, 1], DECBAS)
     obs_D = StreamConverter.get_obs_from_obs(obs_e, False, True)
     d = obs_D.select(channel="D")[0].data
@@ -264,21 +264,21 @@ def test_get_obs_from_obs():
         d, [15 * D2R, 30 * D2R], 9, "Expect D to equal [15 degrees, 30 degrees]", True
     )
 
-    # 2) Call get_obs_from_obs using a decbase of 15 degrees, a H stream of
+    # 2) Call get_obs_from_obs using a decbase of 15 degrees, a U stream of
     #   [cos(15), cos(30)], and a D stream of [15, 30].
     #   Expect a D stream of [sin(15), sin(30)]
     obs_d = obspy.core.Stream()
-    obs_d += __create_trace("H", [cos(15 * D2R), cos(30 * D2R)], DECBAS)
+    obs_d += __create_trace("U", [cos(15 * D2R), cos(30 * D2R)], DECBAS)
     obs_d += __create_trace("D", [15 * D2R, 30 * D2R], DECBAS)
-    obs_d += __create_trace("Z", [1, 1], DECBAS)
+    obs_d += __create_trace("W", [1, 1], DECBAS)
     obs_d += __create_trace("F", [1, 1], DECBAS)
-    obs_E = StreamConverter.get_obs_from_obs(obs_d, True, False)
-    e = obs_E.select(channel="E")[0].data
+    obs_V = StreamConverter.get_obs_from_obs(obs_d, True, False)
+    v = obs_V.select(channel="V")[0].data
     assert_almost_equal(
-        e,
+        v,
         [sin(15 * D2R), sin(30 * D2R)],
         9,
-        "Expect E to equal [sin(15), sin(30)",
+        "Expect V to equal [sin(15), sin(30)",
         True,
     )
 
@@ -298,13 +298,13 @@ def test_verification_data():
     DECBAS = 552.7
     obs_v = obspy.core.Stream()
     obs_v += __create_trace(
-        "H", [20889.55, 20889.57, 20889.74, 20889.86, 20889.91, 20889.81], DECBAS
+        "U", [20889.55, 20889.57, 20889.74, 20889.86, 20889.91, 20889.81], DECBAS
     )
     obs_v += __create_trace(
-        "E", [-21.10, -20.89, -20.72, -20.57, -20.39, -20.12], DECBAS
+        "V", [-21.10, -20.89, -20.72, -20.57, -20.39, -20.12], DECBAS
     )
     obs_v += __create_trace(
-        "Z", [47565.29, 47565.34, 47565.39, 47565.45, 47565.51, 47565.54], DECBAS
+        "W", [47565.29, 47565.34, 47565.39, 47565.45, 47565.51, 47565.54], DECBAS
     )
     obs_v += __create_trace(
         "F", [52485.77, 52485.84, 52485.94, 52486.06, 52486.11, 52486.10], DECBAS
diff --git a/test/algorithm_test/AdjustedAlgorithm_test.py b/test/algorithm_test/AdjustedAlgorithm_test.py
index 220d498c6f1bf6a65e59243523cd73789b6dc95e..f06b4405181222ec7730a5ed02d8ec5fd1f226ef 100644
--- a/test/algorithm_test/AdjustedAlgorithm_test.py
+++ b/test/algorithm_test/AdjustedAlgorithm_test.py
@@ -87,8 +87,8 @@ def test_process_reverse_polarity_AdjustedMatrix():
             ],
             pier_correction=-22,
         ),
-        inchannels=["H", "E"],
-        outchannels=["H", "E"],
+        inchannels=["U", "V"],
+        outchannels=["U", "V"],
     )
 
     # load boulder May 20 files from /etc/ directory
@@ -101,7 +101,7 @@ def test_process_reverse_polarity_AdjustedMatrix():
     adjusted = a.process(raw)
 
     assert_streams_almost_equal(
-        adjusted=adjusted, expected=expected, channels=["H", "E"]
+        adjusted=adjusted, expected=expected, channels=["U", "V"]
     )
 
 
@@ -141,8 +141,8 @@ def test_process_reverse_polarity_statefile():
     # load adjusted data transform matrix and pier correction
     a = AdjustedAlgorithm(
         statefile="etc/adjusted/adjbou_state_HE_.json",
-        inchannels=["H", "E"],
-        outchannels=["H", "E"],
+        inchannels=["U", "V"],
+        outchannels=["U", "V"],
     )
 
     # load boulder May 20 files from /etc/ directory
@@ -155,7 +155,7 @@ def test_process_reverse_polarity_statefile():
     adjusted = a.process(raw)
 
     assert_streams_almost_equal(
-        adjusted=adjusted, expected=expected, channels=["H", "E"]
+        adjusted=adjusted, expected=expected, channels=["U", "V"]
     )
 
 
diff --git a/test/algorithm_test/FilterAlgorithm_test.py b/test/algorithm_test/FilterAlgorithm_test.py
index b4c21d45afc0581d78d340d7310cdf6cdeb1e30a..9e16a14dd315db2b78727825e603fa0680a8cfd9 100644
--- a/test/algorithm_test/FilterAlgorithm_test.py
+++ b/test/algorithm_test/FilterAlgorithm_test.py
@@ -305,6 +305,37 @@ def test_align_trace():
     assert_equal(starttime, UTCDateTime("2020-08-31T02:29:30"))
 
 
+def test_get_input_interval():
+    """algorithm_test.FilterAlgorithm_test.test_get_input_interval()
+    validates trimming of processing interval at 1Hz, 1-minute, 1-hour and 1-day
+    """
+    original_starttime = original_endtime = UTCDateTime("2021-06-30T12:12:00Z")
+    # 1Hz
+    starttime, endtime = FilterAlgorithm(
+        input_sample_period=0.1, output_sample_period=1.0
+    ).get_input_interval(start=original_starttime, end=original_endtime)
+    assert starttime == UTCDateTime("2021-06-30T12:11:53.9Z")
+    assert endtime == UTCDateTime("2021-06-30T12:12:06.1Z")
+    # 1-minute
+    starttime, endtime = FilterAlgorithm(
+        input_sample_period=1.0, output_sample_period=60.0
+    ).get_input_interval(start=original_starttime, end=original_endtime)
+    assert starttime == UTCDateTime("2021-06-30T12:11:15Z")
+    assert endtime == UTCDateTime("2021-06-30T12:12:45Z")
+    # 1-hour
+    starttime, endtime = FilterAlgorithm(
+        input_sample_period=60.0, output_sample_period=3600.0
+    ).get_input_interval(start=original_starttime, end=original_endtime)
+    assert starttime == UTCDateTime("2021-06-30T12:00:00Z")
+    assert endtime == UTCDateTime("2021-06-30T12:59:00Z")
+    # 1-day
+    starttime, endtime = FilterAlgorithm(
+        input_sample_period=60.0, output_sample_period=86400.0
+    ).get_input_interval(start=original_starttime, end=original_endtime)
+    assert starttime == UTCDateTime("2021-06-30T00:00:00Z")
+    assert endtime == UTCDateTime("2021-06-30T23:59:00Z")
+
+
 def test_get_nearest__oneday_average():
     """algorithm_test.FilterAlgorithm_test.test_get_nearest__oneday_average()
     Tests get_nearest_time for minute to day
diff --git a/test/algorithm_test/XYZAlgorithm_test.py b/test/algorithm_test/XYZAlgorithm_test.py
index 67be143a880a3ed11b5de59c0d711e0773c205d7..bc1edb16a3160b12512fca5f302e375a6276055b 100644
--- a/test/algorithm_test/XYZAlgorithm_test.py
+++ b/test/algorithm_test/XYZAlgorithm_test.py
@@ -13,9 +13,9 @@ def test_xyzalgorithm_process():
     """
     algorithm = XYZAlgorithm("obs", "geo")
     timeseries = Stream()
-    timeseries += __create_trace("H", [1, 1])
-    timeseries += __create_trace("E", [1, 1])
-    timeseries += __create_trace("Z", [1, 1])
+    timeseries += __create_trace("U", [1, 1])
+    timeseries += __create_trace("V", [1, 1])
+    timeseries += __create_trace("W", [1, 1])
     timeseries += __create_trace("F", [1, 1])
     outputstream = algorithm.process(timeseries)
     assert_equal(outputstream[0].stats.channel, "X")
@@ -28,7 +28,7 @@ def test_xyzalgorithm_channels():
     informat/outformat during instantiation.
     """
     algorithm = XYZAlgorithm("obs", "geo")
-    inchannels = ["H", "E", "Z", "F"]
+    inchannels = ["U", "V", "W", "F"]
     outchannels = ["X", "Y", "Z", "F"]
     assert_equal(algorithm.get_input_channels(), inchannels)
     assert_equal(algorithm.get_output_channels(), outchannels)
@@ -38,13 +38,13 @@ def test_xyzalgorithm_limited_channels():
     """XYZAlgorithm_test.test_xyzalgorithm_limited_channels()
 
     confirms that only the required channels are necessary for processing
-    ie. 'H' and 'E' are only needed to get 'X' and 'Y' without 'Z' or 'F'
+    ie. 'U' and 'V' are only needed to get 'X' and 'Y' without 'Z' or 'F'
     """
     algorithm = XYZAlgorithm("obs", "mag")
     count = 5
     timeseries = Stream()
-    timeseries += __create_trace("H", [2] * count)
-    timeseries += __create_trace("E", [3] * count)
+    timeseries += __create_trace("U", [2] * count)
+    timeseries += __create_trace("V", [3] * count)
     outstream = algorithm.process(timeseries)
     ds = outstream.select(channel="D")
     # there is 1 trace
@@ -66,14 +66,14 @@ def test_xyzalgorithm_uneccesary_channel_empty():
     """
     algorithm = XYZAlgorithm("obs", "mag")
     timeseries = Stream()
-    timeseries += __create_trace("H", [1, 1])
-    timeseries += __create_trace("E", [1, 1])
-    timeseries += __create_trace("Z", [1, np.NaN])
+    timeseries += __create_trace("U", [1, 1])
+    timeseries += __create_trace("V", [1, 1])
+    timeseries += __create_trace("W", [1, np.NaN])
     timeseries += __create_trace("F", [np.NaN, np.NaN])
     outstream = algorithm.process(timeseries)
     assert_equal(
-        outstream.select(channel="Z")[0].data.all(),
-        timeseries.select(channel="Z")[0].data.all(),
+        outstream.select(channel="W")[0].data.all(),
+        timeseries.select(channel="W")[0].data.all(),
     )
     assert_equal(
         outstream.select(channel="F")[0].data.all(),
diff --git a/test/derived_test.py b/test/derived_test.py
new file mode 100644
index 0000000000000000000000000000000000000000..0dbf55055cf11b21dee724397301846f9b9035f4
--- /dev/null
+++ b/test/derived_test.py
@@ -0,0 +1,84 @@
+from obspy import Stream, UTCDateTime
+
+from geomagio import derived, TimeseriesUtility
+
+
+def test_get_missing_channels():
+    """test.derived_test.test_get_missing_channels()"""
+    starttime = UTCDateTime("2021-07-01T00:00:00Z")
+    endtime = UTCDateTime("2021-07-01T00:01:00Z")
+    timeseries = Stream()
+    for channel in ["U", "V", "W", "F"]:
+        timeseries += TimeseriesUtility.create_empty_trace(
+            starttime=starttime,
+            endtime=endtime,
+            observatory="TEST",
+            channel=channel,
+            type="variation",
+            interval="second",
+            network="NT",
+            station="TEST",
+            location="R0",
+        )
+    assert derived.get_missing_channels(timeseries=timeseries) == [
+        "U",
+        "V",
+        "W",
+        "F",
+    ]
+
+
+def test_get_derived_input_channels():
+    """test.derived_test.test_get_derived_input_channels()"""
+    assert derived.get_derived_input_channels(channel="G", data_type="variation") == [
+        "U",
+        "V",
+        "W",
+        "F",
+    ]
+    assert derived.get_derived_input_channels(channel="G", data_type="adjusted") == [
+        "X",
+        "Y",
+        "Z",
+        "F",
+    ]
+    assert derived.get_derived_input_channels(channel="D", data_type="variation") == [
+        "U",
+        "V",
+    ]
+    assert derived.get_derived_input_channels(channel="X", data_type="variation") == [
+        "U",
+        "V",
+    ]
+    assert derived.get_derived_input_channels(channel="Y", data_type="variation") == [
+        "U",
+        "V",
+    ]
+    assert derived.get_derived_input_channels(channel="H", data_type="adjusted") == [
+        "X",
+        "Y",
+    ]
+    assert derived.get_derived_input_channels(channel="D", data_type="adjusted") == [
+        "X",
+        "Y",
+    ]
+
+
+def test_get_required_channels():
+    """test.derived_test.test_get_required_channels()"""
+    assert derived.get_required_channels(
+        channels=["G", "D"], data_type="variation"
+    ) == ["U", "V", "W", "F"]
+    assert derived.get_required_channels(
+        channels=["X", "Y"], data_type="variation"
+    ) == ["U", "V"]
+    assert derived.get_required_channels(channels=["G", "D"], data_type="adjusted") == [
+        "X",
+        "Y",
+        "Z",
+        "F",
+    ]
+    assert derived.get_required_channels(channels=["H", "D"], data_type="adjusted") == [
+        "X",
+        "Y",
+    ]
diff --git a/test/edge_test/LegacySNCL_test.py b/test/edge_test/LegacySNCL_test.py
index adbcc8a8388ba85a543cd075d2e3bccc2c0e5202..7e456401af2983a459ea55e0f3845bbf80b3ce79 100644
--- a/test/edge_test/LegacySNCL_test.py
+++ b/test/edge_test/LegacySNCL_test.py
@@ -35,7 +35,7 @@ def test_element():
             channel="MVU",
             location="R0",
         ).element
-        == "U"
+        == "H"
     )
     assert (
         LegacySNCL(
@@ -113,17 +113,24 @@ def test_element():
 
 def test_get_channel():
     """edge_test.LegacySNCL_test.test_get_channel()"""
-    assert get_channel(element="D", interval="second") == "SVD"
-    assert get_channel(element="F", interval="minute") == "MSF"
-    assert get_channel(element="H", interval="hour") == "HVH"
-    assert get_channel(element="E-E", interval="day") == "DQE"
-    assert get_channel(element="E-N", interval="minute") == "MQN"
-    assert get_channel(element="SQ", interval="minute") == "MSQ"
-    assert get_channel(element="SV", interval="minute") == "MSV"
-    assert get_channel(element="UK1", interval="minute") == "UK1"
-    assert get_channel(element="DIST", interval="minute") == "MDT"
-    assert get_channel(element="DST", interval="minute") == "MGD"
-    assert get_channel(element="UK1.R0", interval="minute") == "UK1"
+    assert get_channel(data_type="variation", element="D", interval="second") == "SVD"
+    assert get_channel(data_type="variation", element="F", interval="minute") == "MSF"
+    assert get_channel(data_type="variation", element="H", interval="hour") == "HVH"
+    assert get_channel(data_type="variation", element="E-E", interval="day") == "DQE"
+    assert get_channel(data_type="variation", element="E-N", interval="minute") == "MQN"
+    assert get_channel(data_type="variation", element="SQ", interval="minute") == "MSQ"
+    assert get_channel(data_type="variation", element="SV", interval="minute") == "MSV"
+    assert get_channel(data_type="variation", element="UK1", interval="minute") == "UK1"
+    assert (
+        get_channel(data_type="variation", element="DIST", interval="minute") == "MDT"
+    )
+    assert get_channel(data_type="variation", element="DST", interval="minute") == "MGD"
+    assert (
+        get_channel(data_type="variation", element="UK1.R0", interval="minute") == "UK1"
+    )
+    assert get_channel(data_type="variation", element="U", interval="minute") == "MVH"
+    assert get_channel(data_type="variation", element="V", interval="minute") == "MVE"
+    assert get_channel(data_type="variation", element="W", interval="minute") == "MVZ"
 
 
 def test_get_location():
diff --git a/test/edge_test/MiniSeedFactory_test.py b/test/edge_test/MiniSeedFactory_test.py
index bd1367d37ad3487522ddad7f5df879c3c37ad5f5..502300d264684c273f158bb4d80c70c28d6f2b5c 100644
--- a/test/edge_test/MiniSeedFactory_test.py
+++ b/test/edge_test/MiniSeedFactory_test.py
@@ -50,7 +50,7 @@ def test__pre_process():
     processed = MiniSeedInputClient(host=None)._pre_process(stream=Stream(trace))
     assert len(processed) == 2
     for trace in processed:
-        assert trace.data.dtype == "float32"
+        assert trace.data.dtype == "float64"
         stats = trace.stats
         assert stats.npts == 86400
         assert stats.starttime.timestamp % 86400 == 0
@@ -62,7 +62,7 @@ def test__format_miniseed():
     buf = io.BytesIO()
     trace = __create_trace(numpy.arange((86400 * 2) + 1), channel="H")
     MiniSeedInputClient(host=None)._format_miniseed(stream=Stream(trace), buf=buf)
-    block_size = 512
+    block_size = 1024
     data = buf.getvalue()
     n_blocks = int(len(data) / block_size)
     assert n_blocks == 1516