diff --git a/geomagio/adjusted/AdjustedMatrix.py b/geomagio/adjusted/AdjustedMatrix.py
index f8b8999ef87531ddb8811e09e3287cda8e5b1d85..ea24041dc1b597a1e33165468f3dec6c720b36c1 100644
--- a/geomagio/adjusted/AdjustedMatrix.py
+++ b/geomagio/adjusted/AdjustedMatrix.py
@@ -1,11 +1,12 @@
 import numpy as np
 from obspy import UTCDateTime
 from pydantic import BaseModel
-from typing import Any, List, Optional, Tuple
+from typing import Any, List, Optional
 
 from .. import ChannelConverter
 from .. import pydantic_utcdatetime
 from .Metric import Metric
+from ..residual.Reading import Reading, get_absolutes_xyz, get_ordinates
 
 
 class AdjustedMatrix(BaseModel):
@@ -39,26 +40,22 @@ class AdjustedMatrix(BaseModel):
             adjusted = adjusted[0 : len(outchannels)]
         return adjusted
 
-    # TODO: MOVE GET ABSOLUTES/ORDINATES INTO READING
-    def get_metrics(
-        self,
-        ordinates: Tuple[List[float], List[float], List[float]],
-        absolutes: Tuple[List[float], List[float], List[float]],
-    ) -> List[Metric]:
+    def get_metrics(self, readings: List[Reading]) -> List[Metric]:
         """Computes mean absolute error and standard deviation for X, Y, Z, and dF between expected and predicted values.
 
         Attributes
         ----------
-        absolutes: X, Y and Z absolutes
-        ordinates: H, E and Z ordinates
+        readings: list of Readings
         matrix: composed matrix
 
         Outputs
         -------
         metrics: list of Metric objects
         """
-        ordinates = np.vstack((ordinates, np.ones_like(ordinates[0])))
-        predicted = self.matrix @ ordinates
+        absolutes = get_absolutes_xyz(readings=readings)
+        ordinates = get_ordinates(readings=readings)
+        stacked_ordinates = np.vstack((ordinates, np.ones_like(ordinates[0])))
+        predicted = self.matrix @ stacked_ordinates
         metrics = []
         elements = ["X", "Y", "Z", "dF"]
         expected = absolutes + tuple(
diff --git a/geomagio/adjusted/Affine.py b/geomagio/adjusted/Affine.py
index fc3252deaf8e6e6fbb0cf21c2ea2411d59957db1..20100eff92b6fe9b99d1538b8483614f95fc9dd8 100644
--- a/geomagio/adjusted/Affine.py
+++ b/geomagio/adjusted/Affine.py
@@ -5,7 +5,12 @@ from pydantic import BaseModel
 from typing import List, Optional, Tuple
 
 from .. import pydantic_utcdatetime
-from ..residual import Reading
+from ..residual.Reading import (
+    Reading,
+    get_absolutes_xyz,
+    get_baselines,
+    get_ordinates,
+)
 from .AdjustedMatrix import AdjustedMatrix
 
 from .transform import RotationTranslationXY, TranslateOrigins, Transform
@@ -95,7 +100,7 @@ class Affine(BaseModel):
         """
         absolutes = get_absolutes_xyz(readings)
         baselines = get_baselines(readings)
-        ordinates = get_ordinates(readings, self.observatory)
+        ordinates = get_ordinates(readings)
         times = get_times(readings)
         Ms = []
         weights = []
@@ -131,7 +136,7 @@ class Affine(BaseModel):
             matrix=M_composed,
             pier_correction=pier_correction,
         )
-        matrix.metrics = matrix.get_metrics(absolutes=absolutes, ordinates=ordinates)
+        matrix.metrics = matrix.get_metrics(readings=readings)
         return AdjustedMatrix(
             matrix=M_composed,
             pier_correction=pier_correction,
@@ -214,39 +219,6 @@ def filter_iqrs(
     return weights * good
 
 
-def get_absolutes(
-    readings: List[Reading],
-) -> Tuple[List[float], List[float], List[float]]:
-    """Get H, D and Z absolutes"""
-    h_abs = np.array([reading.get_absolute("H").absolute for reading in readings])
-    d_abs = np.array([reading.get_absolute("D").absolute for reading in readings])
-    z_abs = np.array([reading.get_absolute("Z").absolute for reading in readings])
-
-    return (h_abs, d_abs, z_abs)
-
-
-def get_absolutes_xyz(
-    readings: List[Reading],
-) -> Tuple[List[float], List[float], List[float]]:
-    """Get X, Y and Z absolutes from H, D and Z baselines"""
-    h_abs, d_abs, z_abs = get_absolutes(readings)
-    # convert from cylindrical to Cartesian coordinates
-    x_a = h_abs * np.cos(np.radians(d_abs))
-    y_a = h_abs * np.sin(np.radians(d_abs))
-    z_a = z_abs
-    return (x_a, y_a, z_a)
-
-
-def get_baselines(
-    readings: List[Reading],
-) -> Tuple[List[float], List[float], List[float]]:
-    """Get H, D and Z baselines"""
-    h_bas = np.array([reading.get_absolute("H").baseline for reading in readings])
-    d_bas = np.array([reading.get_absolute("D").baseline for reading in readings])
-    z_bas = np.array([reading.get_absolute("Z").baseline for reading in readings])
-    return (h_bas, d_bas, z_bas)
-
-
 def get_epochs(
     epochs: List[float],
     time: UTCDateTime,
@@ -277,31 +249,6 @@ def get_epochs(
     return epoch_start, epoch_end
 
 
-def get_ordinates(
-    readings: List[Reading], observatory: str
-) -> Tuple[List[float], List[float], List[float]]:
-    """Calculates ordinates from absolutes and baselines"""
-    h_abs, d_abs, z_abs = get_absolutes(readings)
-    h_bas, d_bas, z_bas = get_baselines(readings)
-
-    # recreate ordinate variometer measurements from absolutes and baselines
-    h_ord = h_abs - h_bas
-    d_ord = d_abs - d_bas
-    z_ord = z_abs - z_bas
-
-    # WebAbsolutes defines/generates h differently than USGS residual
-    # method spreadsheets. The following should ensure that ordinate
-    # values are converted back to their original raw measurements:
-    # TODO: REMOVE OR FIX IN RESIDUALS
-    e_o = h_abs * d_ord * 60 / 3437.7468
-    if observatory in ["DED", "CMO"]:
-        h_o = np.sqrt(h_ord ** 2 - e_o ** 2)
-    else:
-        h_o = h_ord
-    z_o = z_ord
-    return (h_o, e_o, z_o)
-
-
 def get_times(readings: List[UTCDateTime]):
     return np.array([reading.get_absolute("H").endtime for reading in readings])
 
diff --git a/geomagio/residual/Reading.py b/geomagio/residual/Reading.py
index c6ede9ebf28862ba409e7461964400dd9f5211ea..20a423bf6660f4507ec11ab380401470e18b54c7 100644
--- a/geomagio/residual/Reading.py
+++ b/geomagio/residual/Reading.py
@@ -1,6 +1,7 @@
-from typing import Dict, List, Optional
+from typing import Dict, List, Optional, Tuple
 from typing_extensions import Literal
 
+import numpy as np
 from obspy import Stream, UTCDateTime
 from pydantic import BaseModel
 
@@ -40,6 +41,15 @@ class Reading(BaseModel):
         """
         return [m for m in self.measurements if m.measurement_type == measurement_type]
 
+    def get_absolute(
+        self,
+        element: str,
+    ) -> Optional[Absolute]:
+        for absolute in self.absolutes:
+            if absolute.element == element:
+                return absolute
+        return None
+
     def load_ordinates(
         self,
         observatory: str,
@@ -119,11 +129,60 @@ class Reading(BaseModel):
         ):
             return True
 
-    def get_absolute(
-        self,
-        element: str,
-    ) -> Optional[Absolute]:
-        for absolute in self.absolutes:
-            if absolute.element == element:
-                return absolute
-        return None
+
+def get_absolutes(
+    readings: List[Reading],
+) -> Tuple[List[float], List[float], List[float]]:
+    """Get H, D and Z absolutes"""
+    h_abs = np.array([reading.get_absolute("H").absolute for reading in readings])
+    d_abs = np.array([reading.get_absolute("D").absolute for reading in readings])
+    z_abs = np.array([reading.get_absolute("Z").absolute for reading in readings])
+
+    return (h_abs, d_abs, z_abs)
+
+
+def get_absolutes_xyz(
+    readings: List[Reading],
+) -> Tuple[List[float], List[float], List[float]]:
+    """Get X, Y and Z absolutes from H, D and Z baselines"""
+    h_abs, d_abs, z_abs = get_absolutes(readings)
+    # convert from cylindrical to Cartesian coordinates
+    x_a = h_abs * np.cos(np.radians(d_abs))
+    y_a = h_abs * np.sin(np.radians(d_abs))
+    z_a = z_abs
+    return (x_a, y_a, z_a)
+
+
+def get_baselines(
+    readings: List[Reading],
+) -> Tuple[List[float], List[float], List[float]]:
+    """Get H, D and Z baselines"""
+    h_bas = np.array([reading.get_absolute("H").baseline for reading in readings])
+    d_bas = np.array([reading.get_absolute("D").baseline for reading in readings])
+    z_bas = np.array([reading.get_absolute("Z").baseline for reading in readings])
+    return (h_bas, d_bas, z_bas)
+
+
+def get_ordinates(
+    readings: List[Reading],
+) -> Tuple[List[float], List[float], List[float]]:
+    """Calculates ordinates from absolutes and baselines"""
+    h_abs, d_abs, z_abs = get_absolutes(readings)
+    h_bas, d_bas, z_bas = get_baselines(readings)
+
+    # recreate ordinate variometer measurements from absolutes and baselines
+    h_ord = h_abs - h_bas
+    d_ord = d_abs - d_bas
+    z_ord = z_abs - z_bas
+
+    # WebAbsolutes defines/generates h differently than USGS residual
+    # method spreadsheets. The following should ensure that ordinate
+    # values are converted back to their original raw measurements:
+    # TODO: REMOVE OR FIX IN RESIDUALS
+    e_o = h_abs * d_ord * 60 / 3437.7468
+    if len(readings) > 0 and readings[0].metadata["station"] in ["DED", "CMO"]:
+        h_o = np.sqrt(h_ord ** 2 - e_o ** 2)
+    else:
+        h_o = h_ord
+    z_o = z_ord
+    return (h_o, e_o, z_o)
diff --git a/geomagio/residual/SpreadsheetSummaryFactory.py b/geomagio/residual/SpreadsheetSummaryFactory.py
index 9d1e1f718a8444e4872ee9a2c608ea0b3564d0ab..7a1f5082c11e88c3fd26f5637eb3251e25f049e9 100644
--- a/geomagio/residual/SpreadsheetSummaryFactory.py
+++ b/geomagio/residual/SpreadsheetSummaryFactory.py
@@ -61,7 +61,7 @@ class SpreadsheetSummaryFactory(object):
         date = sheet["I1"].value
         date = f"{date.year}{date.month:02}{date.day:02}"
         return {
-            "observatory": sheet["D49"].value[0:3],
+            "station": sheet["D49"].value[0:3],
             "pier_correction": sheet["C5"].value,
             "instrument": sheet["B3"].value,
             "date": date,